diff --git a/src/Platform/StellaOps.Platform.WebService/Contracts/AssistantModels.cs b/src/Platform/StellaOps.Platform.WebService/Contracts/AssistantModels.cs new file mode 100644 index 000000000..03adbb203 --- /dev/null +++ b/src/Platform/StellaOps.Platform.WebService/Contracts/AssistantModels.cs @@ -0,0 +1,105 @@ +namespace StellaOps.Platform.WebService.Contracts; + +// ─── Tips ──────────────────────────────────────────────────────────────────── + +public sealed record AssistantTipDto( + string TipId, + string Title, + string Body, + AssistantTipActionDto? Action, + string? ContextTrigger); + +public sealed record AssistantTipActionDto(string Label, string Route); + +public sealed record AssistantTipsResponse( + string Greeting, + AssistantTipDto[] Tips, + AssistantTipDto[] ContextTips); + +// ─── Glossary ──────────────────────────────────────────────────────────────── + +public sealed record GlossaryTermDto( + string TermId, + string Term, + string Definition, + string? ExtendedHelp, + string[] RelatedTerms, + string[] RelatedRoutes); + +public sealed record GlossaryResponse(GlossaryTermDto[] Terms); + +// ─── Tours ─────────────────────────────────────────────────────────────────── + +public sealed record TourDto( + string TourId, + string TourKey, + string Title, + string Description, + object[] Steps); + +public sealed record ToursResponse(TourDto[] Tours); + +// ─── User State ────────────────────────────────────────────────────────────── + +public sealed record AssistantUserStateDto( + string[] SeenRoutes, + string[] CompletedTours, + Dictionary TipPositions, + bool Dismissed); + +// ─── Admin ─────────────────────────────────────────────────────────────────── + +public sealed record UpsertAssistantTipRequest( + string RoutePattern, + string? ContextTrigger, + string Locale, + int SortOrder, + string Title, + string Body, + string? ActionLabel, + string? ActionRoute, + string? LearnMoreUrl, + bool IsActive, + string? ProductVersion); + +public sealed record UpsertGlossaryTermRequest( + string Term, + string Locale, + string Definition, + string? ExtendedHelp, + string[] RelatedTerms, + string[] RelatedRoutes, + bool IsActive); + +// Admin listing — includes metadata for editing +public sealed record AssistantTipAdminDto( + string TipId, + string RoutePattern, + string? ContextTrigger, + string Locale, + int SortOrder, + string Title, + string Body, + string? ActionLabel, + string? ActionRoute, + bool IsActive, + string CreatedBy, + string UpdatedAt); + +public sealed record UpsertTourRequest( + string TourKey, + string Title, + string Description, + string Locale, + object[] Steps, + bool IsActive); + +public sealed record TourAdminDto( + string TourId, + string TourKey, + string Title, + string Description, + string Locale, + int StepCount, + bool IsActive, + string CreatedAt); diff --git a/src/Platform/StellaOps.Platform.WebService/Endpoints/AssistantEndpoints.cs b/src/Platform/StellaOps.Platform.WebService/Endpoints/AssistantEndpoints.cs new file mode 100644 index 000000000..1366242ac --- /dev/null +++ b/src/Platform/StellaOps.Platform.WebService/Endpoints/AssistantEndpoints.cs @@ -0,0 +1,229 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using StellaOps.Platform.WebService.Constants; +using StellaOps.Platform.WebService.Contracts; +using StellaOps.Platform.WebService.Services; + +namespace StellaOps.Platform.WebService.Endpoints; + +/// +/// Stella Assistant API — DB-backed, locale-aware contextual help for the mascot. +/// Serves tips, glossary, tours, and persists user state. +/// +public static class AssistantEndpoints +{ + public static IEndpointRouteBuilder MapAssistantEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/api/v1/stella-assistant") + .WithTags("Stella Assistant") + .RequireAuthorization(PlatformPolicies.PreferencesRead); + + // ─── Tips ──────────────────────────────────────────────────────── + + group.MapGet("/tips", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + [FromQuery] string route, + [FromQuery] string? locale, + [FromQuery] string? contexts, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var effectiveLocale = locale ?? "en-US"; + var contextList = string.IsNullOrWhiteSpace(contexts) + ? Array.Empty() + : contexts.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + var result = await store.GetTipsAsync(route, effectiveLocale, contextList, tenantId, ct); + return Results.Ok(result); + }) + .WithName("GetAssistantTips") + .WithSummary("Get contextual tips for a route and locale"); + + // ─── Glossary ──────────────────────────────────────────────────── + + group.MapGet("/glossary", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + [FromQuery] string? locale, + [FromQuery] string? terms, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var effectiveLocale = locale ?? "en-US"; + var termList = string.IsNullOrWhiteSpace(terms) + ? null + : terms.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + var result = await store.GetGlossaryAsync(effectiveLocale, tenantId, termList, ct); + return Results.Ok(result); + }) + .WithName("GetAssistantGlossary") + .WithSummary("Get domain glossary terms for a locale"); + + // ─── User State ────────────────────────────────────────────────── + + group.MapGet("/user-state", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + CancellationToken ct) => + { + var (userId, tenantId) = ResolveUserContext(httpContext); + var state = await store.GetUserStateAsync(userId, tenantId, ct); + return state is not null ? Results.Ok(state) : Results.Ok(new AssistantUserStateDto( + Array.Empty(), Array.Empty(), new Dictionary(), false)); + }) + .WithName("GetAssistantUserState") + .WithSummary("Get user's assistant preferences and seen routes"); + + group.MapPut("/user-state", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + AssistantUserStateDto state, + CancellationToken ct) => + { + var (userId, tenantId) = ResolveUserContext(httpContext); + await store.UpsertUserStateAsync(userId, tenantId, state, ct); + return Results.Ok(); + }) + .WithName("UpdateAssistantUserState") + .WithSummary("Persist user's assistant preferences") + .RequireAuthorization(PlatformPolicies.PreferencesWrite); + + // ─── Tours ─────────────────────────────────────────────────────── + + group.MapGet("/tours", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + [FromQuery] string? locale, + [FromQuery] string? tourKey, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var effectiveLocale = locale ?? "en-US"; + var result = await store.GetToursAsync(effectiveLocale, tenantId, tourKey, ct); + return Results.Ok(result); + }) + .WithName("GetAssistantTours") + .WithSummary("Get guided tour definitions"); + + // ─── Admin CRUD ────────────────────────────────────────────────── + + var admin = app.MapGroup("/api/v1/stella-assistant/admin") + .WithTags("Stella Assistant Admin") + .RequireAuthorization(PlatformPolicies.SetupAdmin); + + admin.MapPost("/tips", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + UpsertAssistantTipRequest request, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var actor = ResolveUserId(httpContext); + var id = await store.UpsertTipAsync(tenantId, request, actor, ct); + return Results.Ok(new { tipId = id }); + }) + .WithName("UpsertAssistantTip") + .WithSummary("Create or update a tip"); + + admin.MapDelete("/tips/{tipId}", async Task( + string tipId, + PostgresAssistantStore store, + CancellationToken ct) => + { + await store.DeactivateTipAsync(tipId, ct); + return Results.Ok(); + }) + .WithName("DeactivateAssistantTip") + .WithSummary("Deactivate a tip"); + + admin.MapGet("/tips", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + [FromQuery] string? locale, + [FromQuery] string? route, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var result = await store.ListAllTipsAsync(tenantId, locale ?? "en-US", route, ct); + return Results.Ok(result); + }) + .WithName("ListAllAssistantTips") + .WithSummary("List all tips for admin editing"); + + admin.MapGet("/tours", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + [FromQuery] string? locale, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var result = await store.ListAllToursAsync(tenantId, locale ?? "en-US", ct); + return Results.Ok(result); + }) + .WithName("ListAllAssistantTours") + .WithSummary("List all tours for admin editing"); + + admin.MapPost("/tours", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + UpsertTourRequest request, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var id = await store.UpsertTourAsync(tenantId, request, ct); + return Results.Ok(new { tourId = id }); + }) + .WithName("UpsertAssistantTour") + .WithSummary("Create or update a guided tour"); + + admin.MapGet("/tours/{tourKey}", async Task( + string tourKey, + HttpContext httpContext, + PostgresAssistantStore store, + [FromQuery] string? locale, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var tour = await store.GetTourByKeyAsync(tenantId, tourKey, locale ?? "en-US", ct); + return tour is not null ? Results.Ok(tour) : Results.NotFound(); + }) + .WithName("GetAssistantTourByKey") + .WithSummary("Get a single tour by key for editing"); + + admin.MapPost("/glossary", async Task( + HttpContext httpContext, + PostgresAssistantStore store, + UpsertGlossaryTermRequest request, + CancellationToken ct) => + { + var tenantId = ResolveTenantId(httpContext); + var id = await store.UpsertGlossaryTermAsync(tenantId, request, ct); + return Results.Ok(new { termId = id }); + }) + .WithName("UpsertGlossaryTerm") + .WithSummary("Create or update a glossary term"); + + return app; + } + + private static string ResolveUserId(HttpContext ctx) + => ctx.User.FindFirst("sub")?.Value + ?? ctx.User.FindFirst("stellaops:user_id")?.Value + ?? "anonymous"; + + private static string ResolveTenantId(HttpContext ctx) + => ctx.Request.Headers["X-Tenant-Id"].FirstOrDefault() ?? "_system"; + + private static (string UserId, string TenantId) ResolveUserContext(HttpContext ctx) + { + var userId = ctx.User.FindFirst("sub")?.Value + ?? ctx.User.FindFirst("stellaops:user_id")?.Value + ?? "anonymous"; + var tenantId = ResolveTenantId(ctx); + return (userId, tenantId); + } +} diff --git a/src/Platform/StellaOps.Platform.WebService/Program.cs b/src/Platform/StellaOps.Platform.WebService/Program.cs index 6057ff79f..8df6dde39 100644 --- a/src/Platform/StellaOps.Platform.WebService/Program.cs +++ b/src/Platform/StellaOps.Platform.WebService/Program.cs @@ -244,6 +244,7 @@ if (!string.IsNullOrWhiteSpace(bootstrapOptions.Storage.PostgresConnectionString builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Auto-migrate platform schemas on startup builder.Services.AddStartupMigrations( @@ -337,6 +338,7 @@ app.Use(async (context, next) => await app.LoadTranslationsAsync(); app.MapLocalizationEndpoints(); +app.MapAssistantEndpoints(); app.MapEnvironmentSettingsEndpoints(); app.MapEnvironmentSettingsAdminEndpoints(); app.MapContextEndpoints(); diff --git a/src/Platform/StellaOps.Platform.WebService/Services/AssistantStore.cs b/src/Platform/StellaOps.Platform.WebService/Services/AssistantStore.cs new file mode 100644 index 000000000..52f11d763 --- /dev/null +++ b/src/Platform/StellaOps.Platform.WebService/Services/AssistantStore.cs @@ -0,0 +1,575 @@ +using System.Text.Json; +using Npgsql; +using StellaOps.Platform.WebService.Contracts; + +namespace StellaOps.Platform.WebService.Services; + +/// +/// PostgreSQL store for the Stella Assistant (tips, greetings, glossary, tours, user state). +/// +public sealed class PostgresAssistantStore +{ + private readonly NpgsqlDataSource _ds; + + public PostgresAssistantStore(NpgsqlDataSource dataSource) + => _ds = dataSource ?? throw new ArgumentNullException(nameof(dataSource)); + + // ─── Tips ──────────────────────────────────────────────────────────── + + /// + /// Resolve tips for a given route, locale, and optional context triggers. + /// Uses longest-prefix matching: /ops/policy/vex/consensus matches before /ops/policy/vex. + /// Falls back to en-US if no tips exist for the requested locale. + /// + public async Task GetTipsAsync( + string route, string locale, string[] contexts, string tenantId, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + + // Greeting + var greeting = await GetGreetingAsync(conn, route, locale, tenantId, ct); + + // Page tips (longest prefix match) + var tips = await GetPageTipsAsync(conn, route, locale, tenantId, ct); + + // Context-triggered tips + var contextTips = contexts.Length > 0 + ? await GetContextTipsAsync(conn, contexts, locale, tenantId, ct) + : Array.Empty(); + + return new AssistantTipsResponse(greeting, tips, contextTips); + } + + private async Task GetGreetingAsync( + NpgsqlConnection conn, string route, string locale, string tenantId, CancellationToken ct) + { + // Try exact match, then progressively shorter prefixes + var prefixes = BuildPrefixes(route); + + const string sql = @" + SELECT greeting_text FROM platform.assistant_greetings + WHERE route_pattern = ANY(@routes) + AND locale = @locale + AND (tenant_id = @tenantId OR tenant_id = '_system') + AND is_active + ORDER BY length(route_pattern) DESC, tenant_id DESC + LIMIT 1"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@routes", prefixes); + cmd.Parameters.AddWithValue("@locale", locale); + cmd.Parameters.AddWithValue("@tenantId", tenantId); + + var result = await cmd.ExecuteScalarAsync(ct) as string; + + // Fallback to en-US if not found + if (result is null && locale != "en-US") + { + cmd.Parameters["@locale"].Value = "en-US"; + result = await cmd.ExecuteScalarAsync(ct) as string; + } + + return result ?? "Hi! I'm Stella, your DevOps guide."; + } + + private async Task GetPageTipsAsync( + NpgsqlConnection conn, string route, string locale, string tenantId, CancellationToken ct) + { + var prefixes = BuildPrefixes(route); + + const string sql = @" + WITH matched AS ( + SELECT t.*, length(t.route_pattern) AS specificity, + ROW_NUMBER() OVER ( + PARTITION BY t.sort_order + ORDER BY length(t.route_pattern) DESC, t.tenant_id DESC + ) AS rn + FROM platform.assistant_tips t + WHERE t.route_pattern = ANY(@routes) + AND t.locale = @locale + AND (t.tenant_id = @tenantId OR t.tenant_id = '_system') + AND t.is_active + AND t.context_trigger IS NULL + ) + SELECT tip_id, title, body, action_label, action_route, context_trigger + FROM matched WHERE rn = 1 + ORDER BY sort_order"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@routes", prefixes); + cmd.Parameters.AddWithValue("@locale", locale); + cmd.Parameters.AddWithValue("@tenantId", tenantId); + + var tips = new List(); + await using var reader = await cmd.ExecuteReaderAsync(ct); + while (await reader.ReadAsync(ct)) + tips.Add(MapTip(reader)); + + // Fallback to en-US + if (tips.Count == 0 && locale != "en-US") + { + await reader.DisposeAsync(); + cmd.Parameters["@locale"].Value = "en-US"; + await using var reader2 = await cmd.ExecuteReaderAsync(ct); + while (await reader2.ReadAsync(ct)) + tips.Add(MapTip(reader2)); + } + + return tips.ToArray(); + } + + private async Task GetContextTipsAsync( + NpgsqlConnection conn, string[] contexts, string locale, string tenantId, CancellationToken ct) + { + const string sql = @" + SELECT tip_id, title, body, action_label, action_route, context_trigger + FROM platform.assistant_tips + WHERE context_trigger = ANY(@contexts) + AND locale = @locale + AND (tenant_id = @tenantId OR tenant_id = '_system') + AND is_active + ORDER BY sort_order"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@contexts", contexts); + cmd.Parameters.AddWithValue("@locale", locale); + cmd.Parameters.AddWithValue("@tenantId", tenantId); + + var tips = new List(); + await using var reader = await cmd.ExecuteReaderAsync(ct); + while (await reader.ReadAsync(ct)) + tips.Add(MapTip(reader)); + + if (tips.Count == 0 && locale != "en-US") + { + await reader.DisposeAsync(); + cmd.Parameters["@locale"].Value = "en-US"; + await using var reader2 = await cmd.ExecuteReaderAsync(ct); + while (await reader2.ReadAsync(ct)) + tips.Add(MapTip(reader2)); + } + + return tips.ToArray(); + } + + // ─── Glossary ──────────────────────────────────────────────────────── + + public async Task GetGlossaryAsync( + string locale, string? tenantId, string[]? terms, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + var tid = tenantId ?? "_system"; + + var sql = @" + SELECT term_id, term, definition, extended_help, related_terms, related_routes + FROM platform.assistant_glossary + WHERE locale = @locale + AND (tenant_id = @tid OR tenant_id = '_system') + AND is_active"; + + if (terms is { Length: > 0 }) + sql += " AND term = ANY(@terms)"; + + sql += " ORDER BY term"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@locale", locale); + cmd.Parameters.AddWithValue("@tid", tid); + if (terms is { Length: > 0 }) + cmd.Parameters.AddWithValue("@terms", terms); + + var result = new List(); + await using var reader = await cmd.ExecuteReaderAsync(ct); + while (await reader.ReadAsync(ct)) + { + result.Add(new GlossaryTermDto( + reader.GetString(0), + reader.GetString(1), + reader.GetString(2), + reader.IsDBNull(3) ? null : reader.GetString(3), + reader.GetFieldValue(4), + reader.GetFieldValue(5))); + } + + // Fallback to en-US + if (result.Count == 0 && locale != "en-US") + { + await reader.DisposeAsync(); + cmd.Parameters["@locale"].Value = "en-US"; + await using var reader2 = await cmd.ExecuteReaderAsync(ct); + while (await reader2.ReadAsync(ct)) + { + result.Add(new GlossaryTermDto( + reader2.GetString(0), + reader2.GetString(1), + reader2.GetString(2), + reader2.IsDBNull(3) ? null : reader2.GetString(3), + reader2.GetFieldValue(4), + reader2.GetFieldValue(5))); + } + } + + return new GlossaryResponse(result.ToArray()); + } + + // ─── User State ────────────────────────────────────────────────────── + + public async Task GetUserStateAsync( + string userId, string tenantId, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = @" + SELECT seen_routes, completed_tours, tip_positions, dismissed + FROM platform.assistant_user_state + WHERE user_id = @userId AND tenant_id = @tenantId"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@userId", userId); + cmd.Parameters.AddWithValue("@tenantId", tenantId); + + await using var reader = await cmd.ExecuteReaderAsync(ct); + if (!await reader.ReadAsync(ct)) return null; + + var tipPositions = reader.IsDBNull(2) + ? new Dictionary() + : JsonSerializer.Deserialize>(reader.GetString(2)) + ?? new Dictionary(); + + return new AssistantUserStateDto( + reader.GetFieldValue(0), + reader.GetFieldValue(1), + tipPositions, + reader.GetBoolean(3)); + } + + public async Task UpsertUserStateAsync( + string userId, string tenantId, AssistantUserStateDto state, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = @" + INSERT INTO platform.assistant_user_state (user_id, tenant_id, seen_routes, completed_tours, tip_positions, dismissed, last_seen_at) + VALUES (@userId, @tenantId, @seenRoutes, @completedTours, @tipPositions::jsonb, @dismissed, now()) + ON CONFLICT (user_id, tenant_id) DO UPDATE SET + seen_routes = EXCLUDED.seen_routes, + completed_tours = EXCLUDED.completed_tours, + tip_positions = EXCLUDED.tip_positions, + dismissed = EXCLUDED.dismissed, + last_seen_at = now()"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@userId", userId); + cmd.Parameters.AddWithValue("@tenantId", tenantId); + cmd.Parameters.AddWithValue("@seenRoutes", state.SeenRoutes); + cmd.Parameters.AddWithValue("@completedTours", state.CompletedTours); + cmd.Parameters.AddWithValue("@tipPositions", JsonSerializer.Serialize(state.TipPositions)); + cmd.Parameters.AddWithValue("@dismissed", state.Dismissed); + + await cmd.ExecuteNonQueryAsync(ct); + } + + // ─── Helpers ───────────────────────────────────────────────────────── + + // ─── Tours ─────────────────────────────────────────────────────────── + + public async Task GetToursAsync( + string locale, string tenantId, string? tourKey, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + var sql = @" + SELECT tour_id, tour_key, title, description, steps + FROM platform.assistant_tours + WHERE locale = @locale + AND (tenant_id = @tid OR tenant_id = '_system') + AND is_active"; + if (tourKey is not null) + sql += " AND tour_key = @tourKey"; + sql += " ORDER BY tour_key"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@locale", locale); + cmd.Parameters.AddWithValue("@tid", tenantId); + if (tourKey is not null) + cmd.Parameters.AddWithValue("@tourKey", tourKey); + + var tours = new List(); + await using var reader = await cmd.ExecuteReaderAsync(ct); + while (await reader.ReadAsync(ct)) + { + var stepsJson = reader.GetString(4); + var steps = JsonSerializer.Deserialize(stepsJson) ?? Array.Empty(); + tours.Add(new TourDto( + reader.GetGuid(0).ToString(), + reader.GetString(1), + reader.GetString(2), + reader.GetString(3), + steps)); + } + + // Fallback to en-US + if (tours.Count == 0 && locale != "en-US") + { + await reader.DisposeAsync(); + cmd.Parameters["@locale"].Value = "en-US"; + await using var r2 = await cmd.ExecuteReaderAsync(ct); + while (await r2.ReadAsync(ct)) + { + var stepsJson = r2.GetString(4); + var steps = JsonSerializer.Deserialize(stepsJson) ?? Array.Empty(); + tours.Add(new TourDto(r2.GetGuid(0).ToString(), r2.GetString(1), r2.GetString(2), r2.GetString(3), steps)); + } + } + + return new ToursResponse(tours.ToArray()); + } + + // ─── Admin CRUD ────────────────────────────────────────────────────── + + public async Task UpsertTipAsync( + string tenantId, UpsertAssistantTipRequest req, string actor, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = @" + INSERT INTO platform.assistant_tips + (route_pattern, context_trigger, locale, sort_order, title, body, + action_label, action_route, learn_more_url, is_active, product_version, + tenant_id, created_by, updated_at) + VALUES (@route, @ctx, @locale, @sort, @title, @body, + @actionLabel, @actionRoute, @learnMore, @active, @version, + @tid, @actor, now()) + ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order + DO UPDATE SET + title = EXCLUDED.title, + body = EXCLUDED.body, + action_label = EXCLUDED.action_label, + action_route = EXCLUDED.action_route, + learn_more_url = EXCLUDED.learn_more_url, + is_active = EXCLUDED.is_active, + product_version = EXCLUDED.product_version, + updated_at = now() + RETURNING tip_id::text"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@route", req.RoutePattern); + cmd.Parameters.AddWithValue("@ctx", (object?)req.ContextTrigger ?? DBNull.Value); + cmd.Parameters.AddWithValue("@locale", req.Locale); + cmd.Parameters.AddWithValue("@sort", req.SortOrder); + cmd.Parameters.AddWithValue("@title", req.Title); + cmd.Parameters.AddWithValue("@body", req.Body); + cmd.Parameters.AddWithValue("@actionLabel", (object?)req.ActionLabel ?? DBNull.Value); + cmd.Parameters.AddWithValue("@actionRoute", (object?)req.ActionRoute ?? DBNull.Value); + cmd.Parameters.AddWithValue("@learnMore", (object?)req.LearnMoreUrl ?? DBNull.Value); + cmd.Parameters.AddWithValue("@active", req.IsActive); + cmd.Parameters.AddWithValue("@version", (object?)req.ProductVersion ?? DBNull.Value); + cmd.Parameters.AddWithValue("@tid", tenantId); + cmd.Parameters.AddWithValue("@actor", actor); + + var result = await cmd.ExecuteScalarAsync(ct); + return result?.ToString() ?? ""; + } + + public async Task DeactivateTipAsync(string tipId, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = "UPDATE platform.assistant_tips SET is_active = FALSE, updated_at = now() WHERE tip_id = @id::uuid"; + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@id", tipId); + await cmd.ExecuteNonQueryAsync(ct); + } + + public async Task ListAllTipsAsync( + string tenantId, string locale, string? route, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + var sql = @" + SELECT tip_id, route_pattern, context_trigger, locale, sort_order, + title, body, action_label, action_route, is_active, + created_by, updated_at::text + FROM platform.assistant_tips + WHERE (tenant_id = @tid OR tenant_id = '_system') + AND locale = @locale"; + if (route is not null) + sql += " AND route_pattern = @route"; + sql += " ORDER BY route_pattern, sort_order"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@tid", tenantId); + cmd.Parameters.AddWithValue("@locale", locale); + if (route is not null) + cmd.Parameters.AddWithValue("@route", route); + + var tips = new List(); + await using var reader = await cmd.ExecuteReaderAsync(ct); + while (await reader.ReadAsync(ct)) + { + tips.Add(new AssistantTipAdminDto( + reader.GetGuid(0).ToString(), + reader.GetString(1), + reader.IsDBNull(2) ? null : reader.GetString(2), + reader.GetString(3), + reader.GetInt32(4), + reader.GetString(5), + reader.GetString(6), + reader.IsDBNull(7) ? null : reader.GetString(7), + reader.IsDBNull(8) ? null : reader.GetString(8), + reader.GetBoolean(9), + reader.GetString(10), + reader.GetString(11))); + } + return tips.ToArray(); + } + + public async Task UpsertGlossaryTermAsync( + string tenantId, UpsertGlossaryTermRequest req, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = @" + INSERT INTO platform.assistant_glossary + (term, locale, definition, extended_help, related_terms, related_routes, is_active, tenant_id) + VALUES (@term, @locale, @def, @ext, @relTerms, @relRoutes, @active, @tid) + ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale + DO UPDATE SET + definition = EXCLUDED.definition, + extended_help = EXCLUDED.extended_help, + related_terms = EXCLUDED.related_terms, + related_routes = EXCLUDED.related_routes, + is_active = EXCLUDED.is_active + RETURNING term_id::text"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@term", req.Term); + cmd.Parameters.AddWithValue("@locale", req.Locale); + cmd.Parameters.AddWithValue("@def", req.Definition); + cmd.Parameters.AddWithValue("@ext", (object?)req.ExtendedHelp ?? DBNull.Value); + cmd.Parameters.AddWithValue("@relTerms", req.RelatedTerms); + cmd.Parameters.AddWithValue("@relRoutes", req.RelatedRoutes); + cmd.Parameters.AddWithValue("@active", req.IsActive); + cmd.Parameters.AddWithValue("@tid", tenantId); + + var result = await cmd.ExecuteScalarAsync(ct); + return result?.ToString() ?? ""; + } + + public async Task ListAllToursAsync( + string tenantId, string locale, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = @" + SELECT tour_id, tour_key, title, description, locale, + jsonb_array_length(steps), is_active, created_at::text + FROM platform.assistant_tours + WHERE (tenant_id = @tid OR tenant_id = '_system') + AND locale = @locale + ORDER BY tour_key"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@tid", tenantId); + cmd.Parameters.AddWithValue("@locale", locale); + + var tours = new List(); + await using var reader = await cmd.ExecuteReaderAsync(ct); + while (await reader.ReadAsync(ct)) + { + tours.Add(new TourAdminDto( + reader.GetGuid(0).ToString(), + reader.GetString(1), + reader.GetString(2), + reader.GetString(3), + reader.GetString(4), + reader.GetInt32(5), + reader.GetBoolean(6), + reader.GetString(7))); + } + return tours.ToArray(); + } + + public async Task UpsertTourAsync( + string tenantId, UpsertTourRequest req, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = @" + INSERT INTO platform.assistant_tours + (tour_key, locale, title, description, steps, is_active, tenant_id) + VALUES (@key, @locale, @title, @desc, @steps::jsonb, @active, @tid) + ON CONFLICT ON CONSTRAINT ux_assistant_tours_key_locale + DO UPDATE SET + title = EXCLUDED.title, + description = EXCLUDED.description, + steps = EXCLUDED.steps, + is_active = EXCLUDED.is_active + RETURNING tour_id::text"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@key", req.TourKey); + cmd.Parameters.AddWithValue("@locale", req.Locale); + cmd.Parameters.AddWithValue("@title", req.Title); + cmd.Parameters.AddWithValue("@desc", req.Description); + cmd.Parameters.AddWithValue("@steps", JsonSerializer.Serialize(req.Steps)); + cmd.Parameters.AddWithValue("@active", req.IsActive); + cmd.Parameters.AddWithValue("@tid", tenantId); + + var result = await cmd.ExecuteScalarAsync(ct); + return result?.ToString() ?? ""; + } + + public async Task GetTourByKeyAsync( + string tenantId, string tourKey, string locale, CancellationToken ct = default) + { + await using var conn = await _ds.OpenConnectionAsync(ct); + const string sql = @" + SELECT tour_id, tour_key, title, description, steps + FROM platform.assistant_tours + WHERE tour_key = @key AND locale = @locale + AND (tenant_id = @tid OR tenant_id = '_system') + LIMIT 1"; + + await using var cmd = new NpgsqlCommand(sql, conn); + cmd.Parameters.AddWithValue("@key", tourKey); + cmd.Parameters.AddWithValue("@locale", locale); + cmd.Parameters.AddWithValue("@tid", tenantId); + + await using var reader = await cmd.ExecuteReaderAsync(ct); + if (!await reader.ReadAsync(ct)) return null; + + var stepsJson = reader.GetString(4); + var steps = JsonSerializer.Deserialize(stepsJson) ?? Array.Empty(); + return new TourDto( + reader.GetGuid(0).ToString(), + reader.GetString(1), + reader.GetString(2), + reader.GetString(3), + steps); + } + + /// Build all route prefixes for longest-prefix matching. + private static string[] BuildPrefixes(string route) + { + // /ops/policy/vex/consensus → ["/ops/policy/vex/consensus", "/ops/policy/vex", "/ops/policy", "/ops", "/"] + var prefixes = new List { route }; + var path = route; + while (path.Length > 1) + { + var lastSlash = path.LastIndexOf('/'); + if (lastSlash <= 0) break; + path = path[..lastSlash]; + prefixes.Add(path); + } + prefixes.Add("/"); + return prefixes.ToArray(); + } + + private static AssistantTipDto MapTip(NpgsqlDataReader reader) + { + var actionLabel = reader.IsDBNull(3) ? null : reader.GetString(3); + var actionRoute = reader.IsDBNull(4) ? null : reader.GetString(4); + var action = actionLabel is not null && actionRoute is not null + ? new AssistantTipActionDto(actionLabel, actionRoute) + : null; + + return new AssistantTipDto( + reader.GetGuid(0).ToString(), + reader.GetString(1), + reader.GetString(2), + action, + reader.IsDBNull(5) ? null : reader.GetString(5)); + } +} diff --git a/src/Platform/__Libraries/StellaOps.Platform.Database/Migrations/Release/060_StellaAssistant.sql b/src/Platform/__Libraries/StellaOps.Platform.Database/Migrations/Release/060_StellaAssistant.sql new file mode 100644 index 000000000..047d198f9 --- /dev/null +++ b/src/Platform/__Libraries/StellaOps.Platform.Database/Migrations/Release/060_StellaAssistant.sql @@ -0,0 +1,105 @@ +-- SPRINT_20260329_007 / Unified Stella Assistant +-- DB-backed contextual tips, glossary, tours, and user state for the Stella mascot. +-- Replaces hardcoded English-only tips with locale-aware, admin-editable content. + +CREATE SCHEMA IF NOT EXISTS platform; + +-- ─── Tips: page/tab-level contextual help ─────────────────────────────────── + +CREATE TABLE IF NOT EXISTS platform.assistant_tips ( + tip_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + route_pattern TEXT NOT NULL, + context_trigger TEXT, + locale VARCHAR(10) NOT NULL DEFAULT 'en-US', + sort_order INT NOT NULL DEFAULT 0, + title TEXT NOT NULL, + body TEXT NOT NULL, + action_label TEXT, + action_route TEXT, + learn_more_url TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + product_version VARCHAR(32), + tenant_id VARCHAR(128) NOT NULL DEFAULT '_system', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + created_by VARCHAR(256) NOT NULL DEFAULT 'system', + + CONSTRAINT ux_assistant_tips_route_ctx_locale_order + UNIQUE (tenant_id, route_pattern, context_trigger, locale, sort_order) +); + +CREATE INDEX IF NOT EXISTS ix_assistant_tips_route_locale + ON platform.assistant_tips (route_pattern, locale) + WHERE is_active; + +CREATE INDEX IF NOT EXISTS ix_assistant_tips_context + ON platform.assistant_tips (context_trigger) + WHERE context_trigger IS NOT NULL AND is_active; + +-- ─── Greetings: per-page greeting text ────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS platform.assistant_greetings ( + greeting_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + route_pattern TEXT NOT NULL, + locale VARCHAR(10) NOT NULL DEFAULT 'en-US', + greeting_text TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + tenant_id VARCHAR(128) NOT NULL DEFAULT '_system', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT ux_assistant_greetings_route_locale + UNIQUE (tenant_id, route_pattern, locale) +); + +-- ─── Glossary: domain term definitions ────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS platform.assistant_glossary ( + term_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + term VARCHAR(128) NOT NULL, + locale VARCHAR(10) NOT NULL DEFAULT 'en-US', + definition TEXT NOT NULL, + extended_help TEXT, + related_terms TEXT[] NOT NULL DEFAULT '{}', + related_routes TEXT[] NOT NULL DEFAULT '{}', + is_active BOOLEAN NOT NULL DEFAULT TRUE, + tenant_id VARCHAR(128) NOT NULL DEFAULT '_system', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT ux_assistant_glossary_term_locale + UNIQUE (tenant_id, term, locale) +); + +CREATE INDEX IF NOT EXISTS ix_assistant_glossary_locale + ON platform.assistant_glossary (locale) + WHERE is_active; + +-- ─── Tours: guided walkthroughs ───────────────────────────────────────────── + +CREATE TABLE IF NOT EXISTS platform.assistant_tours ( + tour_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tour_key VARCHAR(128) NOT NULL, + locale VARCHAR(10) NOT NULL DEFAULT 'en-US', + title TEXT NOT NULL, + description TEXT NOT NULL, + steps JSONB NOT NULL DEFAULT '[]', + is_active BOOLEAN NOT NULL DEFAULT TRUE, + tenant_id VARCHAR(128) NOT NULL DEFAULT '_system', + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + CONSTRAINT ux_assistant_tours_key_locale + UNIQUE (tenant_id, tour_key, locale) +); + +-- ─── User state: per-user mascot preferences ─────────────────────────────── + +CREATE TABLE IF NOT EXISTS platform.assistant_user_state ( + user_id VARCHAR(256) NOT NULL, + tenant_id VARCHAR(128) NOT NULL, + seen_routes TEXT[] NOT NULL DEFAULT '{}', + completed_tours TEXT[] NOT NULL DEFAULT '{}', + tip_positions JSONB NOT NULL DEFAULT '{}', + dismissed BOOLEAN NOT NULL DEFAULT FALSE, + last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(), + + PRIMARY KEY (user_id, tenant_id) +); diff --git a/src/Platform/__Libraries/StellaOps.Platform.Database/Migrations/Release/061_StellaAssistantSeedData.sql b/src/Platform/__Libraries/StellaOps.Platform.Database/Migrations/Release/061_StellaAssistantSeedData.sql new file mode 100644 index 000000000..27e8e4958 --- /dev/null +++ b/src/Platform/__Libraries/StellaOps.Platform.Database/Migrations/Release/061_StellaAssistantSeedData.sql @@ -0,0 +1,1784 @@ +-- SPRINT_20260329_007 / Stella Assistant — Seed Data +-- Populates platform.assistant_greetings, platform.assistant_tips, and +-- platform.assistant_glossary with the default en-US content extracted from +-- the static StellaHelper tips configuration. +-- Idempotent: every INSERT uses ON CONFLICT ... DO NOTHING. + +-- ═══════════════════════════════════════════════════════════════════════════ +-- GREETINGS +-- ═══════════════════════════════════════════════════════════════════════════ + +-- dashboard (/) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/', 'en-US', 'Welcome to your command center! I''m Stella, your DevOps guide. Let me show you around.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- welcome (/welcome) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/welcome', 'en-US', 'Welcome to Stella Ops! Sign in to start managing governed releases with verifiable evidence.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- deployments (/releases/deployments) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/deployments', 'en-US', 'This is where deployments happen! Let me explain how releases move between environments.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- releases (/releases) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases', 'en-US', 'Releases are the heart of Stella Ops. Each one is a verified, immutable bundle of your container images.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- environments (/environments) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/environments', 'en-US', 'This topology map shows how releases flow between your environments.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- readiness (/releases/readiness) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/readiness', 'en-US', 'Readiness shows whether your environments are prepared to accept new releases.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- promotions (/releases/promotions) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/promotions', 'en-US', 'Promotions move releases between environments — with evidence at every step.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- approvals (/releases/approvals) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/approvals', 'en-US', 'The approval queue shows all releases waiting for human sign-off.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- approval-detail (/releases/approvals/) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/approvals/', 'en-US', 'Full decision cockpit — all context needed to approve or reject this promotion.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- hotfixes (/releases/hotfixes) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/hotfixes', 'en-US', 'Hotfixes bypass the normal promotion flow for emergency production fixes.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- investigation-timeline (/releases/investigation/timeline) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/investigation/timeline', 'en-US', 'The investigation timeline helps you trace what happened during a deployment incident.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- deploy-diff (/releases/investigation/deploy-diff) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/investigation/deploy-diff', 'en-US', 'Deploy diff shows exactly what changed between two deployments.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- change-trace (/releases/investigation/change-trace) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/investigation/change-trace', 'en-US', 'Change trace follows the full lineage of a change from commit to deployment.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- release-detail (/releases/detail/) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/detail/', 'en-US', 'This is the full detail view for a single release — everything about it in one place.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- releases-versions (/releases/versions) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/versions', 'en-US', 'Versions track the complete release history for a component.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- promotion-graph (/releases/promotion-graph) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/releases/promotion-graph', 'en-US', 'The promotion graph visualizes all possible promotion paths across environments.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vulnerabilities (/triage) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/triage', 'en-US', 'Time to triage! This is where you review and decide what to do about each vulnerability.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- triage-workspace (/triage/artifacts/) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/triage/artifacts/', 'en-US', 'The triage workspace gives you everything needed to make a decision about this artifact.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- security-posture (/security) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/security', 'en-US', 'Security Posture gives you the big picture — how secure is your entire estate right now?', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- supply-chain (/security/supply-chain-data) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/security/supply-chain-data', 'en-US', 'Supply-chain data is the inventory of everything inside your containers.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- findings (/security/findings) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/security/findings', 'en-US', 'The Findings Explorer lets you dive deep into specific vulnerabilities and compare scans.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- reachability (/security/reachability) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/security/reachability', 'en-US', 'Reachability answers the most important security question: can an attacker actually reach this vulnerability?', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- unknowns (/security/unknowns) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/security/unknowns', 'en-US', 'Unknowns are blind spots — components the scanner couldn''t fully identify.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- scan-image (/security/scan) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/security/scan', 'en-US', 'Ready to scan? Submit a container image reference to get a full security analysis.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex (/ops/policy/vex) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex', 'en-US', 'VEX is one of the most powerful features in Stella. Let me demystify it for you.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-search (/ops/policy/vex/search) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/search', 'en-US', 'Search for existing VEX statements across your organization.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-create (/ops/policy/vex/create) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/create', 'en-US', 'Create a new VEX statement — document why a vulnerability does or doesn''t affect you.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-stats (/ops/policy/vex/stats) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/stats', 'en-US', 'VEX statistics show the health and coverage of your exploitability assessments.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-consensus (/ops/policy/vex/consensus) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/consensus', 'en-US', 'Consensus shows how multiple VEX sources agree (or disagree) about vulnerabilities.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-explorer (/ops/policy/vex/explorer) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/explorer', 'en-US', 'The VEX Explorer lets you browse raw VEX data across all sources.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-conflicts (/ops/policy/vex/conflicts) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/conflicts', 'en-US', 'VEX conflicts occur when sources disagree about a vulnerability''s impact.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-exceptions (/ops/policy/vex/exceptions) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/exceptions', 'en-US', 'Exceptions are temporary policy overrides — accepted risk with an expiration date.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- exception-approvals (/ops/policy/vex/exceptions/approvals) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/exceptions/approvals', 'en-US', 'Exception approvals enforce separation of duties for risk acceptance.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- vex-statement-detail (/ops/policy/vex/search/detail/) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/vex/search/detail/', 'en-US', 'Full details of a single VEX statement — provenance, justification, and evidence.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance (/ops/policy/governance) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance', 'en-US', 'Risk & Governance is where you set the security rules for your organization.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-budget (/ops/policy/governance/budget) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/budget', 'en-US', 'The risk budget dashboard shows how much of your allowed risk capacity is consumed.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-budget-config (/ops/policy/governance/budget/config) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/budget/config', 'en-US', 'Configure risk budget thresholds, scoring weights, and environment-specific overrides.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-profiles (/ops/policy/governance/profiles) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/profiles', 'en-US', 'Risk profiles define different risk tolerances for different contexts.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-profile-new (/ops/policy/governance/profiles/new) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/profiles/new', 'en-US', 'Create a new risk profile — define the security boundaries for an environment type.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-conflicts (/ops/policy/governance/conflicts) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/conflicts', 'en-US', 'Policy conflicts occur when rules contradict each other.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-trust-weights (/ops/policy/governance/trust-weights) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/trust-weights', 'en-US', 'Trust weights determine how much influence each VEX source has in consensus decisions.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-staleness (/ops/policy/governance/staleness) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/staleness', 'en-US', 'Staleness policies control how old advisory data can be before it blocks operations.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-sealed-mode (/ops/policy/governance/sealed-mode) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/sealed-mode', 'en-US', 'Sealed mode locks down policy changes — for production stability or compliance freezes.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-validator (/ops/policy/governance/validator) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/validator', 'en-US', 'Validate your policy rules before activating them.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-impact (/ops/policy/governance/impact-preview) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/impact-preview', 'en-US', 'Impact preview shows what would happen if you activated proposed policy changes.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-schema-playground (/ops/policy/governance/schema-playground) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/schema-playground', 'en-US', 'The schema playground lets you experiment with policy rule syntax interactively.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- governance-schema-docs (/ops/policy/governance/schema-docs) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/governance/schema-docs', 'en-US', 'Schema documentation for the Stella policy DSL.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- simulation (/ops/policy/simulation) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/simulation', 'en-US', 'Shadow mode lets you test policy changes safely before they affect real releases.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- sim-shadow (/ops/policy/simulation/shadow) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/simulation/shadow', 'en-US', 'Shadow mode runs proposed rules alongside active ones — no impact on real releases.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- sim-promotion-gate (/ops/policy/simulation/promotion-gate) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/simulation/promotion-gate', 'en-US', 'Simulate how a specific promotion would be evaluated by your policy gates.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- sim-test-validate (/ops/policy/simulation/test-validate) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/simulation/test-validate', 'en-US', 'Test your policy rules against sample data to verify they work as intended.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- sim-pre-promotion (/ops/policy/simulation/pre-promotion) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/simulation/pre-promotion', 'en-US', 'Pre-promotion review shows the policy evaluation a release would face before you request it.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- sim-effective (/ops/policy/simulation/effective) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/simulation/effective', 'en-US', 'Effective policies show the final merged result of all active packs and overrides.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- sim-exceptions (/ops/policy/simulation/exceptions) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/simulation/exceptions', 'en-US', 'See how active exceptions modify the effective policy.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- policy-audit (/ops/policy/audit) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/audit', 'en-US', 'Every policy action in your organization is recorded here — immutable and signed.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- policy-audit-vex (/ops/policy/audit/vex) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/audit/vex', 'en-US', 'VEX audit trail — every VEX statement created, modified, or revoked.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- policy-audit-events (/ops/policy/audit/log/events) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/audit/log/events', 'en-US', 'All policy-related events in chronological order.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- policy-packs (/ops/policy/packs) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/policy/packs', 'en-US', 'Policy packs are bundles of security rules — like security profiles for your environments.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- evidence-overview (/evidence/overview) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/overview', 'en-US', 'Evidence is what makes Stella unique — every decision has tamper-proof, signed proof.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- decision-capsules (/evidence/capsules) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/capsules', 'en-US', 'Decision Capsules are the crown jewel — complete proof packages for every release decision.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- audit-log (/evidence/audit-log) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/audit-log', 'en-US', 'The audit log captures 200+ event types across the entire platform.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- audit-log-events (/evidence/audit-log/events) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/audit-log/events', 'en-US', 'The full event log — every platform action, searchable and filterable.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- audit-log-export (/evidence/audit-log/export) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/audit-log/export', 'en-US', 'Export audit data for external compliance tools or regulatory submissions.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- export-center (/evidence/exports) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/exports', 'en-US', 'Export evidence bundles for auditors, compliance, or air-gapped environments.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- evidence-replay (/evidence/verify-replay) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/verify-replay', 'en-US', 'Replay & Verify lets you re-run past decisions to prove they were correct.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- evidence-proofs (/evidence/proofs) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/proofs', 'en-US', 'Proof chains link evidence across the entire release lifecycle.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- evidence-threads (/evidence/threads) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/threads', 'en-US', 'Evidence threads group all evidence for a specific package or component.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- workspace-auditor (/evidence/workspaces/auditor/) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/workspaces/auditor/', 'en-US', 'The auditor workspace is optimized for compliance review — evidence-first view.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- workspace-developer (/evidence/workspaces/developer/) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/evidence/workspaces/developer/', 'en-US', 'The developer workspace focuses on remediation — what to fix and how.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- operations-hub (/ops/operations) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations', 'en-US', 'Start your day here! The Operations Hub shows everything that needs your attention right now.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- scheduled-jobs (/ops/operations/jobengine) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/jobengine', 'en-US', 'Jobs run behind the scenes — scans, promotions, scheduled tasks, and recovery.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- feeds-airgap (/ops/operations/feeds-airgap) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/feeds-airgap', 'en-US', 'Feeds power your vulnerability detection. No feeds = no vulnerability matching.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- agent-fleet (/ops/operations/agents) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/agents', 'en-US', 'Agents are the bridge between Stella and your deployment targets.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- signals (/ops/operations/signals) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/signals', 'en-US', 'Signals are runtime probes that collect execution data from your running containers.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- diagnostics (/ops/operations/doctor) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/doctor', 'en-US', 'Your first stop when something seems wrong. Doctor runs 100+ health checks across all services.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- scripts (/ops/scripts) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/scripts', 'en-US', 'Scripts are reusable automation blocks for deployments, health checks, and maintenance.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- scanner-ops (/ops/scanner-ops) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/scanner-ops', 'en-US', 'Scanner operations let you configure scan behavior and analyzer settings.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- offline-kit (/ops/operations/offline-kit) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/offline-kit', 'en-US', 'The Offline Kit packages everything needed for air-gapped deployment.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- dead-letter (/ops/operations/dead-letter) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/dead-letter', 'en-US', 'The dead-letter queue holds failed jobs for investigation and retry.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- aoc-compliance (/ops/operations/aoc) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/aoc', 'en-US', 'AOC (Aggregation-Only Contract) compliance checks verify data integrity boundaries.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- health-slo (/ops/operations/health-slo) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/health-slo', 'en-US', 'SLO monitoring tracks service-level objectives across the platform.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- watchlist (/ops/operations/watchlist) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/watchlist', 'en-US', 'The watchlist lets you track specific CVEs, components, or releases for changes.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- runtime-drift (/ops/operations/drift) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/drift', 'en-US', 'Drift detection catches when deployed containers don''t match what was released.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- event-stream (/ops/operations/event-stream) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/event-stream', 'en-US', 'The real-time event stream shows platform events as they happen.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- ai-runs (/ops/operations/ai-runs) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/ops/operations/ai-runs', 'en-US', 'AI runs show Advisory AI analysis history and results.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- integrations (/setup/integrations) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/integrations', 'en-US', 'Integrations connect Stella to your existing tools — registries, SCM, CI, and more.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- identity-access (/setup/identity-access) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/identity-access', 'en-US', 'Manage who can do what. Least privilege + separation of duties = secure operations.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- identity-providers (/setup/identity-providers) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/identity-providers', 'en-US', 'Configure external identity providers for SSO integration.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- certificates-trust (/setup/trust-signing) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/trust-signing', 'en-US', 'Certificates & Trust is the cryptographic backbone — signing keys, trust anchors, and verification.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- trust-issuers (/setup/trust-signing/issuers) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/trust-signing/issuers', 'en-US', 'Trusted issuers are external parties whose VEX statements and advisories you trust.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- theme-branding (/setup/tenant-branding) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/tenant-branding', 'en-US', 'Customize how Stella Ops looks for your organization.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- user-preferences (/setup/preferences) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/preferences', 'en-US', 'Set your personal display preferences — theme, language, layout, and accessibility.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- notification-settings (/setup/notifications) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/notifications', 'en-US', 'Configure how and when Stella sends notifications.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- usage-limits (/setup/usage) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/usage', 'en-US', 'Usage & limits shows your plan consumption and operational quotas.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- ai-preferences (/setup/ai-preferences) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/setup/ai-preferences', 'en-US', 'Configure Advisory AI behavior and preferences.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- admin-tenants (/console/admin/tenants) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/console/admin/tenants', 'en-US', 'Tenant management for multi-tenant Stella installations.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- admin-roles (/console/admin/roles) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/console/admin/roles', 'en-US', 'Define roles and their associated permission scopes.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- admin-clients (/console/admin/clients) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/console/admin/clients', 'en-US', 'OAuth clients enable service-to-service and CI/CD authentication.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- admin-tokens (/console/admin/tokens) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('/console/admin/tokens', 'en-US', 'API tokens for programmatic access and CI/CD integration.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + +-- default (fallback) +INSERT INTO platform.assistant_greetings (route_pattern, locale, greeting_text, tenant_id) +VALUES ('*', 'en-US', 'Hi! I''m Stella, your DevOps guide. I can help you understand any page in this platform.', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_greetings_route_locale DO NOTHING; + + +-- ═══════════════════════════════════════════════════════════════════════════ +-- TIPS +-- ═══════════════════════════════════════════════════════════════════════════ + +-- ── dashboard (/) ────────────────────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/', 'en-US', 0, 'What is this dashboard?', 'This is your daily operations overview. It shows the health of every environment, open vulnerabilities, SBOM coverage, and feed status — all in real-time. Start each day here.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/', 'en-US', 1, 'What does "SBOM: missing" mean?', 'SBOM stands for Software Bill of Materials — it''s a list of every package, library, and binary inside a container image. "Missing" means no images have been scanned yet. Scan one to populate this!', 'Scan your first image', '/security/scan', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/', 'en-US', 2, 'Understanding severity levels', 'Critical = remotely exploitable, fix immediately. High = significant risk, fix within days. Medium = moderate, next sprint. Low = minimal, track and fix when convenient.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/', 'en-US', 3, 'What are "Feeds"?', 'Feeds are vulnerability databases (NVD, OSV) that Stella syncs automatically. When a new CVE is published, Stella checks if any of your scanned images are affected. Keep feeds fresh!', 'Check feed status', '/ops/operations/feeds-airgap', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/', 'en-US', 4, 'Recommended first steps', '1) Run Diagnostics to verify services are healthy. 2) Connect a container registry. 3) Scan your first image. 4) Review findings. 5) Create your first release.', 'Run Diagnostics', '/ops/operations/doctor', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/', 'en-US', 5, 'The status bar at the top', 'The colored dots at the top right show platform health: Events (real-time stream), Policy (active rules), Evidence (signing), Feed (advisory sync), and Offline (connectivity). Green = good.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/', 'en-US', 6, 'Ctrl+K — your power shortcut', 'Press Ctrl+K to open the command palette. From there you can search anything, jump to any page, run scans, and access quick actions without using the sidebar.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── welcome (/welcome) ──────────────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/welcome', 'en-US', 0, 'What is Stella Ops?', 'A release control plane for container environments that don''t use Kubernetes. It scans your containers, enforces security policies, manages approvals, and keeps signed proof of every release decision.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── deployments (/releases/deployments) ──────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/deployments', 'en-US', 0, 'What is a deployment?', 'A deployment is when a release moves from one environment to another (e.g., Staging to Production). Each deployment goes through configured gates — security scans, policy checks, and human approvals — before it can proceed.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/deployments', 'en-US', 1, 'What are "Gates"?', 'Gates are checkpoints a release must pass before promotion. Think of them like airport security: your release (the passenger) must clear each gate (metal detector, passport check, boarding pass) before boarding (deploying).', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/deployments', 'en-US', 2, 'Pending Approvals', 'When a release passes all automated gates, it may still need human approval. You''ll see pending approvals here. Review the evidence, check the gate status, then Approve or Reject.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/deployments', 'en-US', 3, 'Approve vs Reject — when to use each', 'Approve when: all gates pass, evidence is verified, and you''re confident in the release. Reject when: something looks wrong, gates show warnings you don''t accept, or you need more info.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── releases (/releases) ────────────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases', 'en-US', 0, 'What is a Release?', 'A release bundles one or more container images by their immutable SHA256 digest — not by tag. This ensures you deploy exactly what was scanned. No one can swap the image behind your back.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases', 'en-US', 1, 'Understanding the columns', 'Gates: PASS/WARN/BLOCK status of security checks. Risk: aggregate vulnerability severity. Evidence: whether cryptographic proof exists. Status: Draft → Ready → Deployed (or Failed).', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases', 'en-US', 2, 'Why "Evidence: Missing" matters', 'Missing evidence means no Decision Capsule exists for this release. Without evidence, you can''t prove your release decision to auditors. Scans and policy evaluations generate evidence automatically.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases', 'en-US', 3, 'The Promote action', 'Click "Promote" to move a release to the next environment. Stella will evaluate all gates, collect approvals, and record the decision as signed evidence. The entire chain is auditable.', 'Create a new release', '/releases/new', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── environments (/environments) ────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/environments', 'en-US', 0, 'Reading the topology map', 'Each box is an environment — an isolated deployment target with its own security policies and approval rules. Arrows show promotion paths: releases flow from Dev → Staging → Production.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/environments', 'en-US', 1, 'Why you can''t skip environments', 'Releases must pass through each environment in order. This ensures every release is tested in staging before production. The gates at each environment may have different requirements.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/environments', 'en-US', 2, 'Environment health colors', 'Green = healthy, everything running fine. Yellow = degraded, some issues detected. Red = blocked, releases can''t be promoted here. Grey/Unknown = no agent reporting from this environment.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── readiness (/releases/readiness) ─────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/readiness', 'en-US', 0, 'What is Readiness?', 'Readiness checks if deployment targets are online, have enough resources, and meet policy requirements. Think of it like pre-flight checks: is the runway clear, is there fuel, is the crew ready?', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/readiness', 'en-US', 1, 'Empty readiness?', 'If you see no data, it means no deployment agents are connected or no targets are configured yet. Deploy an agent to your first host to start seeing readiness data.', 'Deploy an Agent', '/ops/operations/agents', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── promotions (/releases/promotions) ───────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/promotions', 'en-US', 0, 'Promotion flow', 'Select a release → choose target environment → Stella evaluates all gates (security scan, policy rules, approvals) → on PASS, the release is deployed → evidence is sealed and signed.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── approvals (/releases/approvals) ─────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/approvals', 'en-US', 0, 'Approval decision cockpit', 'Click any approval to open the full decision cockpit with tabs: Overview, Gates, Security, Reachability, Ops/Data, Evidence, Replay/Verify, History. Everything you need to decide is right here.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/approvals', 'en-US', 1, 'Separation of duties', 'You can''t approve your own releases. The system enforces that the creator and approver are different people. This prevents a single person from pushing unchecked code to production.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── approval-detail (/releases/approvals/) ──────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/approvals/', 'en-US', 0, 'What to check before approving', '1) Gates tab: all gates should be PASS or WARN (not BLOCK). 2) Security tab: review open vulnerabilities and VEX coverage. 3) Reachability: are criticals reachable? 4) Evidence: is the proof chain complete?', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/approvals/', 'en-US', 1, 'Replay/Verify tab', 'This tab lets you deterministically replay the policy evaluation. If you get the same result as the automated evaluation, you know the decision wasn''t tampered with.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── hotfixes (/releases/hotfixes) ───────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/hotfixes', 'en-US', 0, 'When to use hotfixes', 'Only for production emergencies that can''t wait for the normal Dev → Stage → Prod flow. Hotfixes still go through gates, but may have relaxed approval requirements and faster timeouts.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/hotfixes', 'en-US', 1, 'Hotfix audit trail', 'Every hotfix is flagged in the audit log. If your organization has SLA requirements for emergency change documentation, the evidence is automatically captured.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── investigation-timeline (/releases/investigation/timeline) ───────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/investigation/timeline', 'en-US', 0, 'Incident investigation', 'See the chronological sequence of events: when was the release created, who approved it, what gates evaluated, when did deployment start, what errors occurred. Essential for post-mortem analysis.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── deploy-diff (/releases/investigation/deploy-diff) ───────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/investigation/deploy-diff', 'en-US', 0, 'Using deploy diff', 'Compare the current deployment against the previous one. See: new container images, changed configurations, added/removed components, and vulnerability delta. Answers: "What''s different about this deploy?"', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── change-trace (/releases/investigation/change-trace) ─────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/investigation/change-trace', 'en-US', 0, 'End-to-end traceability', 'From git commit → CI build → image push → scan → release → promotion → deployment. Every step has evidence. If something went wrong, trace backwards to find where.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── release-detail (/releases/detail/) ──────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/detail/', 'en-US', 0, 'Release detail tabs', 'Overview: summary and metadata. Gates: security and policy gate results. Evidence: signed proof bundles. Components: container images in this release. History: promotion and approval timeline.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── releases-versions (/releases/versions) ──────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/versions', 'en-US', 0, 'Versions vs Releases', 'A Version is a specific build of a component (e.g., api-gateway v2.1.0). A Release bundles one or more versions together for deployment. Think: versions = ingredients, release = the recipe.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── promotion-graph (/releases/promotion-graph) ─────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/releases/promotion-graph', 'en-US', 0, 'Reading the graph', 'Nodes are environments. Edges are allowed promotion paths. Edge labels show gate requirements. Thick edges = frequently used paths. Red edges = currently blocked paths.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vulnerabilities (/triage) ───────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/triage', 'en-US', 0, 'What does "triage" mean?', 'Triage means deciding what to do about each vulnerability finding: Fix it (create a task), Accept the risk (with documented justification), or mark it Not Applicable (with proof). Every decision becomes auditable evidence.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/triage', 'en-US', 1, 'The triage workflow', '1) Select an artifact (scanned container image). 2) Review findings by severity — criticals first. 3) For each: fix, accept risk, or mark not-applicable. 4) When all criticals/highs are addressed, the release becomes "Ready".', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/triage', 'en-US', 2, 'What is an "artifact"?', 'An artifact is a scanned container image, identified by its SHA256 digest. Each artifact has an SBOM (what''s inside it) and findings (what''s wrong with it).', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/triage', 'en-US', 3, 'Opening a Workspace', 'Click "Open Workspace" to get a full investigation view for an artifact — SBOM, findings, reachability evidence, and VEX statements all in one place.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── triage-workspace (/triage/artifacts/) ───────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/triage/artifacts/', 'en-US', 0, 'Workspace tabs', 'Evidence: unified view of all evidence for selected finding. Overview: finding summary. Reachability: can this vuln be reached? Policy: what do rules say? Delta: what changed from baseline? Attestations: signed proofs.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/triage/artifacts/', 'en-US', 1, 'Making a triage decision', 'For each finding: 1) Check severity and reachability. 2) Read the advisory. 3) Check if a VEX statement exists. 4) Decide: fix, accept risk (create exception), or mark not-applicable (create VEX). 5) Your decision becomes evidence.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/triage/artifacts/', 'en-US', 2, 'The Delta tab', 'Delta shows what''s NEW compared to a baseline. A "!" badge means changes were detected. Focus on delta findings — they represent regression from your known-good state.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── security-posture (/security) ────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security', 'en-US', 0, 'Risk Posture score', 'Your risk posture is calculated from unresolved vulnerabilities weighted by severity and reachability. Target MEDIUM or lower for production environments. HIGH means you have critical findings needing attention.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security', 'en-US', 1, 'VEX Coverage', 'VEX Coverage shows how many findings have formal exploitability statements. Higher coverage = fewer "unknown" findings = better signal-to-noise ratio in your security data.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security', 'en-US', 2, '"Start a scan" tip', 'If SBOM health is empty, you need to scan container images first. That populates everything: vulnerability data, reachability analysis, and supply-chain coverage.', 'Scan an image', '/security/scan', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── supply-chain (/security/supply-chain-data) ──────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/supply-chain-data', 'en-US', 0, 'What is Supply-Chain Data?', 'It''s the complete list of OS packages, language libraries, native binaries, and dependency trees inside your container images. This powers vulnerability matching, license compliance, and drift detection.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/supply-chain-data', 'en-US', 1, 'Why is it empty?', 'Supply-chain data is generated when you scan a container image. The scanner extracts every component and maps dependencies. Scan your first image to populate this view.', 'Scan an image', '/security/scan', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/supply-chain-data', 'en-US', 2, 'SBOM formats', 'Stella generates SBOMs in industry-standard formats: SPDX 3.0 and CycloneDX 1.7. These can be exported and shared with customers, auditors, or regulatory bodies.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── findings (/security/findings) ───────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/findings', 'en-US', 0, 'What is a "baseline"?', 'A baseline is a known-good reference scan. When you select one, Stella shows what CHANGED — new vulnerabilities added, old ones fixed. This "Smart-Diff" separates real regressions from inherited noise.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/findings', 'en-US', 1, 'Comparison evidence', 'When you compare scans against a baseline, Stella generates comparison evidence showing exactly what risk changed. This is powerful for release decisions: "Is this release MORE or LESS risky than what''s already deployed?"', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── reachability (/security/reachability) ───────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/reachability', 'en-US', 0, 'Why reachability matters', 'A critical CVE in a library you imported but never call is noise. Reachability analysis traces whether vulnerable code is actually callable from your application''s entry points. This can turn 200 "criticals" into 12 real ones.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/reachability', 'en-US', 1, 'Hybrid analysis approach', 'Stella uses three layers: Static analysis (traces call graphs in your code), Runtime signals (observes what actually executes in practice), and Confidence scoring (from "theoretical" to "confirmed exploitable").', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/reachability', 'en-US', 2, 'Coverage percentage', 'Coverage shows what percentage of your codebase has been analyzed. Higher = more confident decisions. Target >80% for production. Low coverage means more unknowns in your risk assessment.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── unknowns (/security/unknowns) ───────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/unknowns', 'en-US', 0, 'What are Unknowns?', 'Unknowns are stripped binaries without debug symbols, obfuscated code, or packages from sources not in any advisory database. You can''t assess risk for something you can''t identify.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/unknowns', 'en-US', 1, 'How to resolve them', 'Options: Upload debug symbols for binary analysis, add manual annotations about what you know, use Advisory AI for identification, or accept the risk with documented justification.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/unknowns', 'en-US', 2, 'Zero unknowns is good!', 'If you see all zeros — great! Your entire supply chain is identified. All components can be matched against vulnerability databases for accurate risk assessment.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── scan-image (/security/scan) ─────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/scan', 'en-US', 0, 'What happens when you scan', '1) Image layers are pulled and analyzed. 2) SBOM is generated (all packages, libs, binaries). 3) Vulnerabilities matched against feeds. 4) Reachability analysis checks exploitability. 5) Results appear in Security Posture within ~2-5 minutes.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/scan', 'en-US', 1, 'Digest vs Tag', 'Use a digest (sha256:abc...) instead of a tag (:latest) when possible. Tags can be overwritten — someone could push a different image to the same tag. Digests are immutable and guarantee you scan exactly what you intend.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/security/scan', 'en-US', 2, 'Format examples', 'registry.example.com/myapp:v2.1.0 or ghcr.io/org/service@sha256:abc123... or docker.io/library/nginx:1.25 — any standard OCI image reference works.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex (/ops/policy/vex) ───────────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex', 'en-US', 0, 'What is VEX?', 'VEX (Vulnerability Exploitability eXchange) is a formal way to say: "Yes, our software uses this library, but this specific vulnerability doesn''t affect us because [reason]." It''s like a doctor''s note for vulnerabilities.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex', 'en-US', 1, 'VEX statuses explained', 'Affected = vulnerability impacts your software, action needed. Not Affected = vulnerable code path is unreachable in your config. Fixed = patched. Under Investigation = still determining impact.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex', 'en-US', 2, 'Why VEX reduces noise by 60-80%', 'Without VEX, every scan produces hundreds of theoretically-true but practically-irrelevant findings. VEX lets you document WHY certain findings aren''t exploitable, and auto-suppress them in future scans.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex', 'en-US', 3, 'Creating your first VEX statement', 'Go to Vulnerabilities → select a finding → click "Create VEX Statement" → choose status and provide justification. The statement is cryptographically signed and stored as auditable evidence.', 'View Vulnerabilities', '/triage/artifacts', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex', 'en-US', 4, 'VEX Consensus', 'When multiple sources publish VEX statements about the same vulnerability (vendor, scanner, your team), Stella uses trust-weighted consensus to determine the effective status. Higher-trust sources carry more weight.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-search (/ops/policy/vex/search) ─────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/search', 'en-US', 0, 'How to search', 'Search by CVE ID (e.g., CVE-2024-1234), package name, or product name. Results show all VEX statements from all sources — your team, vendors, and community.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/search', 'en-US', 1, 'Statement sources', 'Statements come from: your team (manual creation), upstream vendors (imported), scanner analysis (auto-generated), and community databases. Each carries a trust score.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-create (/ops/policy/vex/create) ─────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/create', 'en-US', 0, 'Creating a VEX statement', 'Select the vulnerability (CVE), choose your product/component, set the status (Affected/Not Affected/Fixed/Under Investigation), and provide justification. The statement is cryptographically signed automatically.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/create', 'en-US', 1, 'Justification is key', 'A good justification explains WHY, not just WHAT. "Not affected because the vulnerable function requires TLS 1.0 which we disabled in config" is better than "Not affected — reviewed."', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/create', 'en-US', 2, 'Impact of your statement', 'Once created, your VEX statement automatically suppresses the finding in future scans. It also feeds into VEX consensus for the broader community if you choose to share.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-stats (/ops/policy/vex/stats) ───────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/stats', 'en-US', 0, 'What to look for', 'High "Not Affected" count = you''ve documented why most findings are noise. High "Investigating" = assessment backlog. Zero "Affected" is rare — if you see it, check whether your team is actually triaging.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/stats', 'en-US', 1, 'Coverage trend', 'VEX coverage should increase over time as your team documents more findings. Flat or declining coverage may indicate the team has stopped triaging, which increases risk.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-consensus (/ops/policy/vex/consensus) ───────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/consensus', 'en-US', 0, 'How consensus works', 'When multiple sources publish VEX statements about the same vulnerability, Stella weighs them by trust score. A vendor saying "not affected" (trust: 0.9) outweighs a community report (trust: 0.5). The effective status is the trust-weighted result.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/consensus', 'en-US', 1, 'Conflicts', 'When sources disagree (vendor says "not affected", scanner says "affected"), a conflict is raised. Review conflicts in the Conflicts tab — they require human judgment to resolve.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/consensus', 'en-US', 2, 'Trust scores', 'Each VEX issuer has a trust score (0-1) based on: Authority (are they the vendor?), Accuracy (historical correctness), Timeliness (how fast they respond), Verification (do they provide evidence?).', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-explorer (/ops/policy/vex/explorer) ─────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/explorer', 'en-US', 0, 'Raw vs effective', 'This view shows raw VEX statements as received — unmodified by consensus or policy. Use this to audit what each source actually said, trace provenance, and verify statement integrity.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-conflicts (/ops/policy/vex/conflicts) ───────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/conflicts', 'en-US', 0, 'What creates a conflict?', 'Vendor says "not affected" but your scanner detects reachable code paths. Or two vendors publish contradictory statements. Conflicts require human review — they can''t be auto-resolved.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/conflicts', 'en-US', 1, 'Resolving conflicts', 'Review the evidence from each source. Check reachability data. Consider trust scores. Then either: override with your own VEX statement, adjust trust weights, or escalate for expert review.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-exceptions (/ops/policy/vex/exceptions) ────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/exceptions', 'en-US', 0, 'What is a policy exception?', 'When a vulnerability is real and affects you, but you can''t fix it right now, create an exception. It documents: what risk you''re accepting, why, who approved it, and when it expires. Think of it as a time-limited "known issue."', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/exceptions', 'en-US', 1, 'Exception lifecycle', 'Create → requires approval (separation of duties) → Active until expiry → Expired (finding re-surfaces). Exceptions are auditable evidence — auditors can see exactly what was accepted and when.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/exceptions', 'en-US', 2, 'Approval requirements', 'Exceptions typically require approval from someone with a higher role than the requester. Critical exceptions may need multiple approvers. This prevents individuals from silently accepting major risks.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── exception-approvals (/ops/policy/vex/exceptions/approvals) ──────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/exceptions/approvals', 'en-US', 0, 'Your approval queue', 'Pending exceptions await your review. For each: check the vulnerability severity, read the justification, review the proposed expiration, then Approve or Reject with your reasoning.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── vex-statement-detail (/ops/policy/vex/search/detail/) ───────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/vex/search/detail/', 'en-US', 0, 'Verifying a statement', 'Check: Who issued it? What trust score do they have? Is the justification specific or vague? Does the evidence (reachability, config) support the claim? Statements without evidence should be treated with lower confidence.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance (/ops/policy/governance) ─────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance', 'en-US', 0, 'What is a Risk Budget?', 'Think of it like a credit limit for security debt. It measures what percentage of your allowed risk capacity is consumed. When it exceeds thresholds (70% = warning, 90% = critical), promotions may be blocked.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance', 'en-US', 1, 'Budget vs. hard blocks', 'The risk budget is a soft gate — it warns and can block, but you can configure overrides. Some rules (like "no known-exploited CVEs in production") can be hard blocks with no override possible.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance', 'en-US', 2, 'Top Contributors', 'The chart shows which vulnerabilities, components, or images contribute most to your risk budget. Focus remediation efforts here for the biggest risk reduction.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-budget (/ops/policy/governance/budget) ───────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/budget', 'en-US', 0, 'Reading the budget chart', 'The trend chart shows budget consumption over time. Spikes indicate new vulnerabilities or expired exceptions. A steadily rising trend means security debt is accumulating faster than you''re remediating.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/budget', 'en-US', 1, 'Budget thresholds', '70% = warning (advisory, logged). 90% = critical (can block promotions). 100% = hard stop (no promotions until budget is reduced). Thresholds are configurable per environment.', NULL, NULL, 'budget-exceeded', '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/budget', 'en-US', 2, 'Top contributors', 'Focus remediation on the top contributors — fixing one high-impact issue often reduces the budget more than fixing ten low-impact ones. The chart shows which CVEs or components consume the most budget.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-budget-config (/ops/policy/governance/budget/config) ──────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/budget/config', 'en-US', 0, 'Tuning your budget', 'Start conservative (lower thresholds) and loosen as your team builds VEX coverage. Too tight = constant false blocks. Too loose = meaningless. The budget should accurately reflect organizational risk tolerance.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-profiles (/ops/policy/governance/profiles) ───────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/profiles', 'en-US', 0, 'Why profiles exist', 'Production needs tight controls. Dev can be looser. A "SOC 2 Prod" profile might allow zero critical CVEs, while a "Dev Fast" profile allows highs with justification. Profiles map to environments.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-profile-new (/ops/policy/governance/profiles/new) ────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/profiles/new', 'en-US', 0, 'Profile design tips', 'Start from an existing profile and modify. Key decisions: max severity allowed, reachability requirements (require proof?), VEX coverage minimum, approval count, and exception policies.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-conflicts (/ops/policy/governance/conflicts) ─────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/conflicts', 'en-US', 0, 'What causes policy conflicts?', 'Two packs defining different severity thresholds for the same environment. Or a global rule that contradicts an environment-specific override. Stella detects these automatically.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/conflicts', 'en-US', 1, 'Resolution strategies', 'Option 1: Merge rules (most restrictive wins). Option 2: Add priority/precedence. Option 3: Remove the conflicting rule. The resolution wizard guides you through each option.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-trust-weights (/ops/policy/governance/trust-weights) ─────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/trust-weights', 'en-US', 0, 'Setting trust weights', 'Vendor statements (they know their code best) typically get 0.8-0.9. Your own team assessments: 0.7-0.9 (depending on expertise). Community reports: 0.3-0.5. Scanner auto-generated: 0.4-0.6.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-staleness (/ops/policy/governance/staleness) ─────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/staleness', 'en-US', 0, 'Why staleness matters', 'If your NVD feed is 7 days old, you might miss critical CVEs published yesterday. The staleness budget defines the max acceptable age before promotions are blocked or warnings are raised.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/staleness', 'en-US', 1, 'Air-gap consideration', 'In air-gapped environments, feeds can''t auto-sync. Set realistic staleness budgets — typically 7-14 days for air-gap, with mandatory manual updates during the window.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-sealed-mode (/ops/policy/governance/sealed-mode) ─────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/sealed-mode', 'en-US', 0, 'What sealed mode does', 'When sealed, no policy changes can be made without explicit override. Use during: production release windows, compliance audit periods, or when you want to freeze the security posture.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-validator (/ops/policy/governance/validator) ──────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/validator', 'en-US', 0, 'How validation works', 'The validator checks your rules for: syntax errors, logical contradictions, unreachable conditions, and potential false-positive rates. Fix issues here before deploying rules to production.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-impact (/ops/policy/governance/impact-preview) ───────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/impact-preview', 'en-US', 0, 'Using impact preview', 'Before activating new rules, see how many existing releases would be affected. A rule that blocks 80% of current releases is probably too aggressive. Aim for targeted impact.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-schema-playground (/ops/policy/governance/schema-playground) + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/schema-playground', 'en-US', 0, 'Learning Stella DSL', 'Stella DSL is the policy language. Try rules like: `deny if severity == "critical" and reachability > 0.7` or `require approval_count >= 2 when environment == "production"`. The playground shows results instantly.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── governance-schema-docs (/ops/policy/governance/schema-docs) ─────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/governance/schema-docs', 'en-US', 0, 'Key schema concepts', 'Inputs: severity, reachability, vex_status, environment, component. Operators: ==, !=, >, <, in, contains. Actions: deny, warn, require, allow. Conditions: when, if, unless.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── simulation (/ops/policy/simulation) ─────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation', 'en-US', 0, 'What is Shadow Mode?', 'Shadow mode runs proposed policy rules alongside active ones and shows where they would produce different decisions — without actually blocking anything. Test before you enforce!', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation', 'en-US', 1, 'Use cases', '"If I tighten the severity threshold, how many releases would be blocked?" or "Will this new compliance rule break existing deployments?" Shadow mode answers these safely.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── sim-shadow (/ops/policy/simulation/shadow) ──────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation/shadow', 'en-US', 0, 'How shadow mode works', 'Enable shadow mode, then every real promotion is evaluated twice: once with active rules (real outcome) and once with proposed rules (shadow outcome). Differences are highlighted so you can see the impact before going live.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── sim-promotion-gate (/ops/policy/simulation/promotion-gate) ──────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation/promotion-gate', 'en-US', 0, 'Gate simulation', 'Select a release and target environment, then see exactly which gates would pass, warn, or block. This answers: "If I promote this right now, what would happen?" without actually promoting.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── sim-test-validate (/ops/policy/simulation/test-validate) ────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation/test-validate', 'en-US', 0, 'Writing test cases', 'Create sample inputs (mock release with known CVEs, specific severity levels) and verify your rules produce the expected outcome. Like unit tests for your security policy.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── sim-pre-promotion (/ops/policy/simulation/pre-promotion) ────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation/pre-promotion', 'en-US', 0, 'Why pre-check', 'Avoid failed promotions by checking first. This view shows all gates, their current state, and what evidence is still needed. Fix issues before requesting the promotion.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── sim-effective (/ops/policy/simulation/effective) ────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation/effective', 'en-US', 0, 'Why this matters', 'When you have multiple policy packs plus environment overrides plus exceptions, it''s hard to know what the ACTUAL rules are. This view flattens everything into the final effective policy.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── sim-exceptions (/ops/policy/simulation/exceptions) ──────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/simulation/exceptions', 'en-US', 0, 'Exception impact', 'Each active exception creates a "hole" in your policy. This view shows exactly what''s being exempted, by whom, until when, and what the policy would say without the exception.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── policy-audit (/ops/policy/audit) ────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/audit', 'en-US', 0, 'What gets audited?', 'Promotions (when a release moved), Approvals (who approved what), Rejections (what was blocked and why), and Simulations (shadow mode comparisons). Events are generated automatically.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/audit', 'en-US', 1, 'Immutable audit trail', 'Unlike regular logs, audit events in Stella are cryptographically signed. They can''t be modified or deleted after creation. This meets compliance requirements for regulated industries.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── policy-audit-vex (/ops/policy/audit/vex) ───────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/audit/vex', 'en-US', 0, 'VEX audit events', 'Track: who created each statement, what justification was given, whether consensus changed, and if any conflicts were resolved. Essential for proving your VEX process to auditors.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── policy-audit-events (/ops/policy/audit/log/events) ─────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/audit/log/events', 'en-US', 0, 'Key events to watch', 'Policy pack activation/deactivation, rule changes, budget threshold breaches, exception approvals/rejections, and sealed mode toggles. Set alerts for unexpected off-hours changes.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── policy-packs (/ops/policy/packs) ───────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/packs', 'en-US', 0, 'What is a Policy Pack?', 'A policy pack is a collection of rules defining what your organization allows in releases. Examples: "Production Strict" (no criticals, 2 approvers), "Dev Relaxed" (allow highs, single approver).', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/packs', 'en-US', 1, 'Pack lifecycle', 'Create a pack → write rules in Stella DSL or YAML → simulate against existing releases → review and approve → activate. Active packs are evaluated on every promotion.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/policy/packs', 'en-US', 2, 'Setting a baseline', 'One pack should be set as your baseline — the default rules that apply everywhere. Additional packs can override or extend the baseline per-environment.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── evidence-overview (/evidence/overview) ──────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/overview', 'en-US', 0, 'What is Evidence?', 'Evidence is the signed, cryptographic record of every scan, decision, approval, and deployment. Unlike logs that can be edited, evidence is sealed — providing audit-grade proof for regulators, customers, or your future self.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/overview', 'en-US', 1, 'Evidence types', 'Scan evidence (SBOM + findings), Policy evidence (rules evaluated + outcomes), Approval evidence (who + when + why), Deployment evidence (what was deployed where), and Proof Chains (linking everything together).', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/overview', 'en-US', 2, 'Offline verification', 'Evidence bundles are self-contained. You can verify them without network access using bundled trust roots. Ship evidence to an air-gapped environment and it''s still verifiable.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── decision-capsules (/evidence/capsules) ──────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/capsules', 'en-US', 0, 'What is a Decision Capsule?', 'A sealed package containing: exact SBOM at scan time, vulnerability findings, VEX statements, reachability evidence, policy rules evaluated, approval records, and cryptographic signatures over everything.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/capsules', 'en-US', 1, 'Why capsules matter', 'If an auditor asks "why did you release this?" — hand them the capsule. It''s self-contained, offline-verifiable, and can be deterministically replayed to prove the decision was correct at that point in time.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/capsules', 'en-US', 2, 'Deterministic replay', 'Capsules can be "replayed" — feed the same inputs through the same policy and verify you get the same outputs. This proves the decision wasn''t manipulated.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── audit-log (/evidence/audit-log) ────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/audit-log', 'en-US', 0, 'What gets logged?', 'Release scans and promotions, policy changes and activations, VEX statements and consensus decisions, integration configuration changes, and identity/access management events.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/audit-log', 'en-US', 1, 'Anomaly detection', 'Check the Timeline tab for chronological event visualization. The Correlation tab helps link related events across modules. Unusual patterns (bulk approvals, off-hours changes) are highlighted.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── audit-log-events (/evidence/audit-log/events) ──────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/audit-log/events', 'en-US', 0, 'Searching events', 'Filter by: module (releases, policy, identity), action type (create, approve, delete), actor (who did it), timestamp range, and resource ID. Use this for incident investigation or compliance audits.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/audit-log/events', 'en-US', 1, 'Export for compliance', 'Export filtered events as JSON or CSV for external audit tools. Exported data includes cryptographic proof that events weren''t modified after creation.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── audit-log-export (/evidence/audit-log/export) ──────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/audit-log/export', 'en-US', 0, 'Export formats', 'JSON (structured, machine-readable), CSV (spreadsheet-friendly), or StellBundle (signed, verifiable). Choose based on your auditor''s requirements.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── export-center (/evidence/exports) ───────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/exports', 'en-US', 0, 'Export profiles', 'StellBundle: signed audit bundle with DSSE envelopes for external auditors. Daily Compliance: automated daily reports with SBOMs and scans. Audit Bundle: complete evidence for external review.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/exports', 'en-US', 1, 'When to export', 'Before external audits (SOC 2, ISO 27001), when sharing security posture with customers, for regulatory submissions, or when transferring evidence to air-gapped environments.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── evidence-replay (/evidence/verify-replay) ──────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/verify-replay', 'en-US', 0, 'Deterministic replay', 'Feed the same frozen inputs (SBOM, feeds, policy version) through the same evaluation engine. If you get the same output, the original decision is proven correct and untampered.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/verify-replay', 'en-US', 1, 'When to replay', 'During audits ("prove this release was properly evaluated"), incident response ("was the gate evaluation correct at the time?"), or trust verification ("has evidence been tampered with?").', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── evidence-proofs (/evidence/proofs) ──────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/proofs', 'en-US', 0, 'What is a proof chain?', 'A chain of cryptographic hashes linking: scan evidence → policy evaluation → approval record → deployment evidence. If any link is modified, the chain breaks. This proves end-to-end integrity.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── evidence-threads (/evidence/threads) ────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/threads', 'en-US', 0, 'Thread lookup', 'Search by package URL (purl) to see every scan, VEX statement, policy decision, and attestation related to that package across all releases and environments.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── workspace-auditor (/evidence/workspaces/auditor/) ───────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/workspaces/auditor/', 'en-US', 0, 'Auditor lens', 'This view prioritizes: signed evidence bundles, proof chain verification, policy compliance status, and exportable artifacts. Designed for external auditors who need to verify your security posture.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── workspace-developer (/evidence/workspaces/developer/) ───────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/evidence/workspaces/developer/', 'en-US', 0, 'Developer lens', 'This view prioritizes: vulnerability details, affected code paths, fix recommendations, upgrade paths, and patch availability. Designed for developers who need to remediate findings.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── operations-hub (/ops/operations) ────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations', 'en-US', 0, 'Daily ops workflow', 'Check blocking issues first (these prevent releases), then pending operator actions, then review budget health across categories. Items marked "Open" need your action.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations', 'en-US', 1, 'Budget categories', 'Blocking Sub: items preventing releases. Blocking: potential blockers. Events: platform events. Health: service status. Supply & Airgap: feed freshness. Capacity: resource usage.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations', 'en-US', 2, 'Critical diagnostics', 'The bottom section shows critical diagnostic results. If any services are unhealthy, they''ll appear here. Click to open the full Diagnostics page for remediation steps.', 'Full Diagnostics', '/ops/operations/doctor', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── scheduled-jobs (/ops/operations/jobengine) ──────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/jobengine', 'en-US', 0, 'What creates jobs?', 'Jobs are created automatically when releases are promoted, scans are triggered, or scheduled tasks run. You can also create manual jobs for one-off operations.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/jobengine', 'en-US', 1, 'Dead-Letter Recovery', 'When a job fails, it goes to the dead-letter queue. You can retry, inspect the failure, or dismiss it. Don''t let dead letters pile up — they may indicate infrastructure issues.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/jobengine', 'en-US', 2, 'Execution quotas', 'Quotas prevent runaway jobs from consuming all resources. Check token usage and concurrency limits here. If jobs are waiting, you may need to increase quota allocation.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── feeds-airgap (/ops/operations/feeds-airgap) ────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/feeds-airgap', 'en-US', 0, 'What are advisory feeds?', 'Vulnerability databases from NVD (US government), OSV (Google), and other sources. Stella syncs these automatically and matches them against your scanned images to find known vulnerabilities.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/feeds-airgap', 'en-US', 1, 'Feed freshness matters', 'Stale feeds = missed vulnerabilities. If a new CVE was published yesterday and your feeds haven''t synced, Stella won''t flag it. Check "Last Sync" timestamps regularly.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/feeds-airgap', 'en-US', 2, 'Air-gap mode', 'For environments without internet, Stella can package feeds, images, and tools into offline bundles. Import them on the air-gapped side. The staleness budget controls how old feeds can be before blocking promotions.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── agent-fleet (/ops/operations/agents) ────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/agents', 'en-US', 0, 'What are agents?', 'Lightweight services you install on Docker hosts, VMs, or other targets. They receive deployment instructions from Stella and execute them (docker-compose up, docker run, etc.), reporting results back with evidence.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/agents', 'en-US', 1, 'How to deploy an agent', 'Download the agent binary, install it on your target host, and register it with Stella using a token. The agent joins a group and starts reporting health. Stella then knows this target is available for deployments.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/agents', 'en-US', 2, 'Agent groups', 'Organize agents into groups by role (web-servers, databases), region (us-east, eu-west), or environment (dev, staging, prod). Groups are used in deployment targeting.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── signals (/ops/operations/signals) ───────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/signals', 'en-US', 0, 'What are Signals?', 'Probes deployed alongside your containers that observe which code paths are actually used at runtime. This powers the "dynamic" layer of reachability analysis — proving which vulnerabilities are actually reachable in practice.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/signals', 'en-US', 1, 'Probe health', 'HEALTHY = probe is collecting data normally. DEGRADED = some data collection issues (high latency, missed events). Check the error rate and last fact timestamp.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/signals', 'en-US', 2, 'Why signals improve security decisions', 'Static analysis says "this code COULD be reached." Signals prove "this code IS being reached." Combined, they dramatically reduce false positives.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── diagnostics (/ops/operations/doctor) ────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/doctor', 'en-US', 0, 'How to use Diagnostics', 'Click "Run All Checks" for a full health sweep. Green = healthy, Red = needs attention. Click any failing check for details and remediation steps. Run this after any infrastructure change.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/doctor', 'en-US', 1, 'Common issues', 'Database connectivity, Valkey (cache) availability, feed sync failures, signing key expiration, and service memory pressure. Doctor catches these before they become user-facing issues.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/doctor', 'en-US', 2, 'Pro tip: run daily', 'Set up a daily diagnostic run via Scheduled Jobs. It catches drift, expiring certificates, and stale feeds before they cause problems. Prevention > firefighting.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── scripts (/ops/scripts) ──────────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/scripts', 'en-US', 0, 'What are scripts?', 'Pre-built automation that runs as part of deployment workflows. Examples: pre-deploy health checks, database migration validators, container size monitors. You can create custom scripts too.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/scripts', 'en-US', 1, 'Script types', 'Bash = shell scripts. Python (py) = Python scripts. JS = Node.js scripts. Each runs in a sandboxed environment with access to Stella APIs for querying release data.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── scanner-ops (/ops/scanner-ops) ──────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/scanner-ops', 'en-US', 0, 'Scanner config', 'Configure: which language analyzers to enable (Node, Go, .NET, Python, etc.), secret detection rules, binary analysis depth, and SBOM output formats. Changes apply to all future scans.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── offline-kit (/ops/operations/offline-kit) ───────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/offline-kit', 'en-US', 0, 'What''s in an offline kit?', 'Vulnerability feeds (frozen snapshot), Stella container images, scanner analyzers, CLI tools, trust roots for signature verification, and database snapshots. Everything an isolated installation needs.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/offline-kit', 'en-US', 1, 'Delta updates', 'After the initial kit, daily deltas are much smaller (<350MB). Transfer via USB, sneakernet, or approved data diode. The staleness budget controls how often you must update.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── dead-letter (/ops/operations/dead-letter) ───────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/dead-letter', 'en-US', 0, 'Why jobs fail', 'Common causes: target host unreachable, out of disk space, image pull failure, timeout, or policy evaluation error. Each dead letter has full error details and retry history.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/dead-letter', 'en-US', 1, 'Don''t let them pile up', 'A growing dead-letter queue often signals infrastructure problems. Investigate root causes rather than just retrying — the same failure will likely repeat.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── aoc-compliance (/ops/operations/aoc) ────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/aoc', 'en-US', 0, 'What is AOC?', 'AOC ensures that ingestion services (Concelier, Excititor) only store raw facts — never computed severities, consensus, or policy hints. The Policy Engine does all derivation. AOC checks verify this boundary is respected.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── health-slo (/ops/operations/health-slo) ────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/health-slo', 'en-US', 0, 'Key SLOs', 'Scan latency (p95 under 5 minutes), promotion gate evaluation (under 30 seconds), feed sync freshness (under 6 hours), and evidence signing (under 2 seconds). Breaches indicate capacity or infrastructure issues.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── watchlist (/ops/operations/watchlist) ───────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/watchlist', 'en-US', 0, 'Setting up watches', 'Watch a CVE to be notified when new VEX statements or advisories are published. Watch a component to track vulnerability changes. Watch a release to monitor gate status changes.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── runtime-drift (/ops/operations/drift) ───────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/drift', 'en-US', 0, 'What is drift?', 'Drift occurs when the actual container running on a host differs from what Stella last promoted. Causes: manual docker pull, tag mutation, unauthorized deployments. Drift is a security risk — it bypasses your policy gates.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── event-stream (/ops/operations/event-stream) ────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/event-stream', 'en-US', 0, 'Using the stream', 'Watch for: promotion approvals, scan completions, policy violations, and feed sync events in real-time. Useful during deployment windows or incident response to see what''s happening right now.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── ai-runs (/ops/operations/ai-runs) ───────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/ops/operations/ai-runs', 'en-US', 0, 'What Advisory AI does', 'Advisory AI helps analyze vulnerabilities, suggest remediations, and draft VEX justifications. Each run is logged here with inputs, outputs, and confidence scores.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── integrations (/setup/integrations) ──────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/integrations', 'en-US', 0, 'Suggested setup order', '1) Registries (where your images live). 2) Source control (Git repos). 3) Scanner config (what to look for). 4) Release controls (environments + approvals). 5) Notifications (Slack, email, webhooks).', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/integrations', 'en-US', 1, 'Registry first!', 'Connect your container registry first — Docker Hub, Harbor, GitHub Container Registry, AWS ECR, etc. This lets Stella discover and scan your images automatically.', 'Add integration', '/setup/integrations', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/integrations', 'en-US', 2, 'Zero integrations is normal', 'For a fresh install, no integrations means you haven''t connected external tools yet. Stella works without them (you can scan manually), but integrations enable automation.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── identity-access (/setup/identity-access) ───────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/identity-access', 'en-US', 0, 'Built-in roles', 'Admin: full access. Operator: create releases + approve promotions. Viewer: read-only dashboards. Auditor: read + evidence export. Developer: submit scans + view findings.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/identity-access', 'en-US', 1, 'Separation of duties', 'For production promotions, the person who CREATES a release should NOT be the same person who APPROVES it. This prevents a single person from pushing unchecked code to production.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/identity-access', 'en-US', 2, 'API tokens', 'API tokens enable CI/CD integration. Create scoped tokens with the minimum permissions needed. Rotate regularly and monitor usage in the Audit Log.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── identity-providers (/setup/identity-providers) ──────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/identity-providers', 'en-US', 0, 'SSO setup', 'Connect your organization''s identity provider (Azure AD, Okta, Keycloak, etc.) so users can sign in with existing corporate credentials. Supports OIDC and SAML protocols.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── certificates-trust (/setup/trust-signing) ──────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/trust-signing', 'en-US', 0, 'Why signing matters', 'Stella signs everything: scans, decisions, evidence, deployments. This proves evidence hasn''t been tampered with and came from your Stella installation. Without signing, evidence is just data — with signing, it''s proof.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/trust-signing', 'en-US', 1, 'Key rotation', 'Rotate signing keys periodically. During transition, both old and new keys remain valid. Old evidence stays verifiable with the old key. Never delete a key while evidence signed by it still needs verification.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/trust-signing', 'en-US', 2, 'Trusted Issuers', 'External parties whose VEX statements or advisories you trust. Each issuer has a trust score (0-1). Higher trust = more weight in consensus decisions. Manage carefully — trusting a bad source degrades your decisions.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── trust-issuers (/setup/trust-signing/issuers) ───────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/trust-signing/issuers', 'en-US', 0, 'Managing trust', 'Each issuer has a composite trust score (0-1) based on: Authority (0-1), Accuracy (0-1), Timeliness (0-1), Verification (0-1). Only add issuers you''ve vetted. A compromised issuer could inject false "not affected" statements.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/trust-signing/issuers', 'en-US', 1, 'Common issuers', 'Software vendors (highest trust for their own products), CERT organizations, scanner vendors, and community databases. Each should be configured with appropriate trust boundaries.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── theme-branding (/setup/tenant-branding) ────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/tenant-branding', 'en-US', 0, 'Multi-tenant branding', 'Each tenant can have its own logo, colors, and application title. Useful for MSPs or organizations with multiple business units sharing one Stella installation.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── user-preferences (/setup/preferences) ───────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/preferences', 'en-US', 0, 'Layout modes', 'Full-width mode uses all available screen space. Centered mode constrains content to 1400px for readability on ultra-wide monitors. Try both and see what works for your workflow.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── notification-settings (/setup/notifications) ───────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/notifications', 'en-US', 0, 'Notification channels', 'Email: for approval requests and audit events. Slack/Teams: for real-time scan results and promotion status. Webhooks: for CI/CD integration and custom automation.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── usage-limits (/setup/usage) ────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/usage', 'en-US', 0, 'Plan limits', 'Free: 3 environments, 999 scans/month. Plus: 33 environments, 9,999 scans. Pro: 333 environments, 99,999 scans. Business: 3,333 environments, 999,999 scans. All tiers include all features.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── ai-preferences (/setup/ai-preferences) ─────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/setup/ai-preferences', 'en-US', 0, 'AI in Stella', 'Advisory AI assists with: vulnerability analysis, remediation suggestions, VEX statement drafting, and pattern detection. It''s advisory-only — all decisions still require human approval.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── admin-tenants (/console/admin/tenants) ──────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/console/admin/tenants', 'en-US', 0, 'Multi-tenancy', 'Each tenant is a fully isolated workspace with its own users, policies, releases, and evidence. Tenants can''t see each other''s data. Use for: separate business units, client isolation, or dev/prod separation.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── admin-roles (/console/admin/roles) ──────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/console/admin/roles', 'en-US', 0, 'Scope-based permissions', 'Roles are collections of scopes (e.g., release:read, policy:approve, signer:sign). Built-in roles cover common patterns. Custom roles let you create fine-grained access for specific workflows.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── admin-clients (/console/admin/clients) ──────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/console/admin/clients', 'en-US', 0, 'Client types', 'Confidential clients: for backend services that can keep a secret. Public clients: for CLI tools and SPAs. Each client gets specific scopes — never grant more than needed.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── admin-tokens (/console/admin/tokens) ────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('/console/admin/tokens', 'en-US', 0, 'Token best practices', 'Create scoped tokens with minimum required permissions. Set short expiration (30-90 days). Rotate on schedule. Monitor usage in audit logs. Revoke immediately if compromised.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +-- ── default (fallback: *) ───────────────────────────────────────────────── + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('*', 'en-US', 0, 'Need help?', 'Press Ctrl+K to open the command palette and search for anything. Or navigate to the Operations Hub for a prioritized view of what needs your attention.', 'Operations Hub', '/ops/operations', NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + +INSERT INTO platform.assistant_tips (route_pattern, locale, sort_order, title, body, action_label, action_route, context_trigger, tenant_id) +VALUES ('*', 'en-US', 1, 'Quick orientation', 'Release Control = your deployment pipeline. Security = vulnerability scanning and triage. Evidence = audit-grade proof of every decision. Operations = platform health and jobs. Settings = integrations and users.', NULL, NULL, NULL, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tips_route_ctx_locale_order DO NOTHING; + + +-- ═══════════════════════════════════════════════════════════════════════════ +-- GLOSSARY — 30 domain terms (en-US) +-- ═══════════════════════════════════════════════════════════════════════════ + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('SBOM', 'en-US', + 'Software Bill of Materials — a complete inventory of every package, library, and binary inside a container image.', + 'SBOMs are generated automatically when you scan an image. They list OS packages, language dependencies, native binaries, and their versions. SBOMs enable vulnerability matching: without knowing what''s inside a container, you can''t determine what''s vulnerable. Industry-standard formats include SPDX 3.0 and CycloneDX 1.7.', + ARRAY['CVE', 'Supply Chain', 'Finding'], + ARRAY['/security/supply-chain-data', '/security/scan'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('VEX', 'en-US', + 'Vulnerability Exploitability eXchange — a formal statement about whether a vulnerability actually affects a specific product.', + 'VEX statements reduce noise by documenting why certain vulnerabilities are not exploitable in your specific context. Statuses include: Affected, Not Affected, Fixed, and Under Investigation. Multiple VEX sources are combined using trust-weighted consensus to determine the effective exploitability status.', + ARRAY['CVE', 'Reachability', 'Trust Score'], + ARRAY['/ops/policy/vex', '/ops/policy/vex/create'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('CVE', 'en-US', + 'Common Vulnerabilities and Exposures — a unique identifier for a publicly known security vulnerability (e.g., CVE-2024-1234).', + 'CVEs are published in vulnerability databases like NVD and OSV. Each CVE describes a flaw, affected software, severity (CVSS score), and sometimes exploitation details. Stella matches CVEs against your SBOM to identify which containers are affected. Not every CVE is exploitable in your context — that''s where VEX and reachability come in.', + ARRAY['SBOM', 'VEX', 'Finding'], + ARRAY['/triage', '/security/findings'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Reachability', 'en-US', + 'Analysis that determines whether vulnerable code is actually callable from your application''s entry points.', + 'Reachability combines static analysis (tracing call graphs) and dynamic signals (runtime observations) to determine if a vulnerability can actually be reached in practice. A critical CVE in a library you imported but never call is unreachable and poses minimal real risk. Reachability analysis can reduce actionable findings by 60-80%.', + ARRAY['VEX', 'Signal', 'Finding'], + ARRAY['/security/reachability'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Decision Capsule', 'en-US', + 'A sealed, cryptographically signed package containing all evidence and context for a single release decision.', + 'Decision Capsules bundle: the exact SBOM at scan time, vulnerability findings, VEX statements, reachability evidence, policy rules evaluated, approval records, and cryptographic signatures. They are self-contained and offline-verifiable. Capsules can be deterministically replayed to prove the decision was correct at the time it was made.', + ARRAY['Evidence', 'Proof Chain', 'Attestation'], + ARRAY['/evidence/capsules'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Policy Gate', 'en-US', + 'A checkpoint that a release must pass before promotion to the next environment.', + 'Gates evaluate security scans, policy rules, approval requirements, and other conditions. Each gate produces a PASS, WARN, or BLOCK verdict. Gates are configured per-environment — production gates are typically stricter than development gates. All gate evaluations are recorded as signed evidence.', + ARRAY['Promotion', 'Policy Pack', 'Risk Budget'], + ARRAY['/releases/deployments', '/ops/policy/simulation/promotion-gate'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Promotion', 'en-US', + 'The process of moving a release from one environment to the next (e.g., Staging to Production).', + 'Promotions go through a defined pipeline: gate evaluation, policy checks, human approvals (if required), and finally deployment. The entire process is captured as signed evidence. Promotions can be blocked by policy gates, risk budgets, or missing approvals. The promotion graph shows all valid paths between environments.', + ARRAY['Policy Gate', 'Decision Capsule', 'Evidence'], + ARRAY['/releases/promotions', '/releases/promotion-graph'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Digest', 'en-US', + 'A SHA256 hash that uniquely and immutably identifies a container image.', + 'Unlike tags (e.g., :latest, :v2.0), digests cannot be changed or overwritten. When Stella references a container image, it uses the digest to guarantee you''re scanning, promoting, and deploying exactly the same bytes. Tag mutation (someone pushing a different image to the same tag) is a supply-chain attack vector that digests prevent.', + ARRAY['SBOM', 'Supply Chain', 'Attestation'], + ARRAY['/security/scan', '/releases'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Attestation', 'en-US', + 'A cryptographically signed statement asserting that a specific action occurred or a condition was verified.', + 'Attestations in Stella cover scans, policy evaluations, approvals, and deployments. Each attestation includes: what was attested, who attested it, when, and a cryptographic signature proving authenticity. Attestations use the DSSE (Dead Simple Signing Envelope) format and can be verified offline using bundled trust roots.', + ARRAY['DSSE', 'Evidence', 'Proof Chain'], + ARRAY['/evidence/overview', '/setup/trust-signing'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('DSSE', 'en-US', + 'Dead Simple Signing Envelope — a standard format for signing arbitrary payloads with verifiable signatures.', + 'DSSE wraps any payload (evidence, attestation, SBOM) in a signed envelope that includes the payload type, the base64-encoded payload, and one or more signatures. It''s designed to be simple, unambiguous, and resistant to confused-deputy attacks. Stella uses DSSE for all evidence signing.', + ARRAY['Attestation', 'Evidence', 'Decision Capsule'], + ARRAY['/evidence/overview', '/setup/trust-signing'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Evidence', 'en-US', + 'Signed, tamper-proof records of every scan, decision, approval, and deployment in Stella.', + 'Evidence is the foundation of Stella''s auditability guarantee. Unlike regular logs, evidence is cryptographically signed at creation and cannot be modified or deleted. Evidence types include: scan results, policy evaluations, approval records, deployment records, and proof chains linking them together. Evidence bundles can be exported for external auditors.', + ARRAY['Attestation', 'Decision Capsule', 'Proof Chain'], + ARRAY['/evidence/overview', '/evidence/exports'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Finding', 'en-US', + 'A vulnerability discovered during a container scan — a match between a CVE and a component in your SBOM.', + 'Findings represent potential security issues. Each finding links a CVE to a specific package version in a specific container image. Findings have severities (Critical, High, Medium, Low) and can be triaged: fixed, risk-accepted, or marked not-applicable via VEX statements. The reachability layer adds context about whether the finding is actually exploitable.', + ARRAY['CVE', 'SBOM', 'Reachability'], + ARRAY['/security/findings', '/triage'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Policy Pack', 'en-US', + 'A collection of security rules that define what your organization allows in releases.', + 'Policy packs contain rules written in Stella DSL or YAML that evaluate releases against security criteria. Examples: "Production Strict" (no criticals, 2 approvers), "Dev Relaxed" (allow highs, 1 approver). Packs follow a lifecycle: create, simulate, review, activate. Multiple packs can be combined, with one designated as the baseline.', + ARRAY['Policy Gate', 'Risk Budget', 'Shadow Mode'], + ARRAY['/ops/policy/packs', '/ops/policy/governance'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Risk Budget', 'en-US', + 'A configurable limit on how much security risk your organization is willing to accept before blocking releases.', + 'The risk budget measures consumed risk capacity as a percentage. It''s calculated from unresolved vulnerabilities weighted by severity, reachability, and environment. Thresholds (e.g., 70% warning, 90% critical, 100% hard stop) control when promotions are warned or blocked. The budget encourages remediation by making accumulated risk visible and actionable.', + ARRAY['Policy Gate', 'Policy Pack', 'Finding'], + ARRAY['/ops/policy/governance/budget', '/ops/policy/governance/budget/config'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Shadow Mode', 'en-US', + 'A simulation mode that runs proposed policy rules alongside active ones without affecting real releases.', + 'Shadow mode evaluates every real promotion twice: once with active rules (producing the real outcome) and once with proposed rules (producing a shadow outcome). Differences are logged and displayed so you can see the impact of rule changes before activating them. This prevents accidental mass-blocking of releases due to overly aggressive rule changes.', + ARRAY['Policy Pack', 'Policy Gate', 'Risk Budget'], + ARRAY['/ops/policy/simulation/shadow', '/ops/policy/simulation'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Baseline', 'en-US', + 'A known-good reference scan used for comparison to detect regressions.', + 'When you set a baseline, future scans are compared against it to show what changed: new vulnerabilities added, old ones fixed, components added or removed. This "Smart-Diff" approach separates real regressions from inherited noise. Baselines are also used in policy packs as the default rule set that applies everywhere before environment-specific overrides.', + ARRAY['Finding', 'SBOM', 'Policy Pack'], + ARRAY['/security/findings', '/ops/policy/packs'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Feed', 'en-US', + 'A vulnerability advisory database (NVD, OSV, etc.) that Stella syncs to match known CVEs against your images.', + 'Feeds are the source of vulnerability intelligence. Stella syncs multiple feeds automatically and matches their contents against SBOMs from scanned images. Feed freshness directly impacts security: stale feeds mean missed vulnerabilities. The staleness budget controls how old feeds can be before operations are blocked.', + ARRAY['CVE', 'Staleness', 'Air-gap'], + ARRAY['/ops/operations/feeds-airgap'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Agent', 'en-US', + 'A lightweight service installed on deployment targets that receives instructions from Stella and executes deployments.', + 'Agents bridge Stella to your infrastructure. They run on Docker hosts, VMs, or other targets, executing deployment commands (docker-compose up, docker run, etc.) and reporting results back with evidence. Agents are organized into groups by role, region, or environment for deployment targeting.', + ARRAY['Promotion', 'Signal', 'Evidence'], + ARRAY['/ops/operations/agents'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Signal', 'en-US', + 'Runtime probes deployed alongside containers that observe which code paths are actually executed.', + 'Signals provide the dynamic layer of reachability analysis. While static analysis says "this code COULD be reached," signals prove "this code IS being reached" based on actual runtime behavior. This dramatically reduces false positives by confirming or denying theoretical reachability with real execution data.', + ARRAY['Reachability', 'Agent', 'Finding'], + ARRAY['/ops/operations/signals'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Advisory', 'en-US', + 'A security notice from a vendor or database describing a vulnerability, affected versions, and recommended actions.', + 'Advisories are published by vendors, CERTs, and vulnerability databases. They describe the flaw, list affected software versions, provide severity ratings, and recommend fixes or mitigations. Stella ingests advisories through feeds and uses them for vulnerability matching and VEX consensus.', + ARRAY['CVE', 'Feed', 'VEX'], + ARRAY['/ops/operations/feeds-airgap', '/security/advisories-vex'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Triage', 'en-US', + 'The process of reviewing vulnerability findings and deciding what action to take for each one.', + 'Triage involves evaluating each finding''s severity, reachability, and context, then deciding: fix (create a remediation task), accept risk (create an exception with justification), or mark not-applicable (create a VEX statement with evidence). Every triage decision becomes auditable evidence. Prioritize criticals and highs first.', + ARRAY['Finding', 'VEX', 'Exception'], + ARRAY['/triage', '/triage/artifacts/'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Supply Chain', 'en-US', + 'The complete dependency tree of software components inside your container images.', + 'Supply chain encompasses OS packages, language libraries, native binaries, transitive dependencies, and build tools. Supply chain security ensures these components are trustworthy, unmodified, and free of known vulnerabilities. Stella maps the full supply chain through SBOM generation, enabling vulnerability matching, license compliance, and drift detection.', + ARRAY['SBOM', 'Digest', 'Finding'], + ARRAY['/security/supply-chain-data'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Deterministic Replay', 'en-US', + 'Re-running a past policy evaluation with the same frozen inputs to verify the original decision was correct.', + 'Deterministic replay takes a Decision Capsule''s inputs (SBOM, feeds snapshot, policy version, VEX state) and feeds them through the same evaluation engine. If the output matches the original decision, it proves the decision was correct and untampered at the time it was made. This is essential for compliance audits and incident investigations.', + ARRAY['Decision Capsule', 'Evidence', 'Proof Chain'], + ARRAY['/evidence/verify-replay', '/evidence/capsules'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Proof Chain', 'en-US', + 'A sequence of cryptographic hashes linking evidence across the entire release lifecycle.', + 'Proof chains connect scan evidence to policy evaluations to approval records to deployment evidence using cryptographic hashes. If any link is modified after creation, the chain breaks and the tampering is detectable. This provides end-to-end integrity guarantees for the entire release process.', + ARRAY['Evidence', 'Decision Capsule', 'Attestation'], + ARRAY['/evidence/proofs'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Trust Score', 'en-US', + 'A numeric value (0-1) representing how much confidence to place in a VEX source''s statements.', + 'Trust scores are composite values based on: Authority (is the source the vendor?), Accuracy (historical correctness of their statements), Timeliness (how quickly they respond to new CVEs), and Verification (do they provide evidence?). Higher trust means more weight in VEX consensus decisions. Vendor statements typically score 0.8-0.9, while community reports score 0.3-0.5.', + ARRAY['VEX', 'Reachability', 'Advisory'], + ARRAY['/ops/policy/governance/trust-weights', '/setup/trust-signing/issuers'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Air-gap', 'en-US', + 'A network-isolated environment with no internet connectivity, requiring manual transfer of data and updates.', + 'Air-gapped environments are common in regulated industries (defense, critical infrastructure, finance). Stella supports air-gap deployments through offline kits that package feeds, images, tools, and trust roots for manual transfer. The staleness budget controls how old advisory data can be in air-gapped contexts before operations are blocked.', + ARRAY['Offline Kit', 'Staleness', 'Feed'], + ARRAY['/ops/operations/offline-kit', '/ops/operations/feeds-airgap'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Offline Kit', 'en-US', + 'A self-contained package of feeds, images, tools, and trust roots for air-gapped Stella deployments.', + 'Offline kits bundle everything an isolated installation needs: frozen vulnerability feed snapshots, Stella container images, scanner analyzers, CLI tools, trust roots for signature verification, and database snapshots. After the initial kit (~several GB), daily delta updates are much smaller (<350MB) and can be transferred via USB or data diode.', + ARRAY['Air-gap', 'Feed', 'Staleness'], + ARRAY['/ops/operations/offline-kit'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Staleness', 'en-US', + 'How old advisory feed data is, measured from the last successful sync.', + 'Staleness directly impacts security coverage: stale feeds mean potentially missed CVEs. The staleness budget defines the maximum acceptable age before warnings or blocks are triggered. For online environments, feeds typically sync every few hours. For air-gapped environments, staleness budgets are wider (7-14 days) to accommodate manual transfer schedules.', + ARRAY['Feed', 'Air-gap', 'Risk Budget'], + ARRAY['/ops/policy/governance/staleness', '/ops/operations/feeds-airgap'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Sealed Mode', 'en-US', + 'A governance state that locks down all policy changes, requiring explicit override to modify rules.', + 'When sealed mode is active, no policy changes can be made through normal workflows. This is used during production release windows (to prevent mid-deploy rule changes), compliance audit periods (to freeze the security posture), or critical operations. Sealed mode toggles are logged in the audit trail and may require elevated privileges to override.', + ARRAY['Policy Pack', 'Policy Gate', 'Risk Budget'], + ARRAY['/ops/policy/governance/sealed-mode'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +INSERT INTO platform.assistant_glossary (term, locale, definition, extended_help, related_terms, related_routes, tenant_id) +VALUES ('Exception', 'en-US', + 'A temporary, approved override that accepts a known vulnerability risk for a defined period.', + 'Exceptions document: what vulnerability risk is being accepted, why it can''t be fixed now, who approved it, and when it expires. They require approval from someone with a higher role than the requester (separation of duties). When an exception expires, the finding re-surfaces for triage. All exceptions are recorded as auditable evidence.', + ARRAY['Triage', 'VEX', 'Risk Budget'], + ARRAY['/ops/policy/vex/exceptions', '/ops/policy/vex/exceptions/approvals'], + '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_glossary_term_locale DO NOTHING; + +-- ═══════════════════════════════════════════════════════════════════════════ +-- TOURS +-- ═══════════════════════════════════════════════════════════════════════════ + +-- Tour 1: First Setup (6 steps) +INSERT INTO platform.assistant_tours (tour_key, locale, title, description, steps, tenant_id) +VALUES ('first-setup', 'en-US', 'Getting Started with Stella Ops', + 'A quick walkthrough to set up your first scan, release, and security pipeline.', + '[ + {"stepOrder": 0, "route": "/ops/operations/doctor", "title": "Run Diagnostics", "body": "First, let''s make sure all platform services are healthy. Click ''Run All Checks'' to verify your installation.", "action": {"label": "Run Checks", "route": "/ops/operations/doctor"}}, + {"stepOrder": 1, "route": "/setup/integrations", "title": "Connect a Registry", "body": "Connect your container registry (Docker Hub, Harbor, GitHub CR, AWS ECR) so Stella can discover and scan your images.", "action": {"label": "Add Integration", "route": "/setup/integrations"}}, + {"stepOrder": 2, "route": "/security/scan", "title": "Scan Your First Image", "body": "Enter a container image reference (e.g., nginx:1.25) and submit. Stella will generate an SBOM, find vulnerabilities, and assess reachability.", "action": {"label": "Scan Image", "route": "/security/scan"}}, + {"stepOrder": 3, "route": "/triage/artifacts", "title": "Review Findings", "body": "See what Stella found in your image. Triage each finding: fix, accept risk, or mark not-applicable. Every decision becomes auditable evidence.", "action": {"label": "Open Triage", "route": "/triage/artifacts"}}, + {"stepOrder": 4, "route": "/releases/new", "title": "Create a Release", "body": "Bundle your scanned image into a tracked release. Releases use immutable SHA256 digests — no tag mutation possible.", "action": {"label": "New Release", "route": "/releases/new"}}, + {"stepOrder": 5, "route": "/ops/policy/packs", "title": "Set Up Policies", "body": "Define security rules for your environments. Start with a baseline pack, then customize per environment (stricter for production, relaxed for dev).", "action": {"label": "Policy Packs", "route": "/ops/policy/packs"}} + ]'::jsonb, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tours_key_locale DO NOTHING; + +-- Tour 2: Scan Workflow (5 steps) +INSERT INTO platform.assistant_tours (tour_key, locale, title, description, steps, tenant_id) +VALUES ('scan-workflow', 'en-US', 'Understanding the Scan Pipeline', + 'Learn how container images flow through scanning, analysis, and evidence generation.', + '[ + {"stepOrder": 0, "route": "/security/scan", "title": "Submit an Image", "body": "Every scan starts with an OCI image reference. Use a digest (sha256:...) for immutability, or a tag for convenience. Stella resolves tags to digests at scan time."}, + {"stepOrder": 1, "route": "/security/supply-chain-data", "selector": "[role=tablist]", "title": "SBOM Generation", "body": "The scanner extracts every component: OS packages, language libraries (11 ecosystems), native binaries, and their dependency trees. This becomes your Software Bill of Materials."}, + {"stepOrder": 2, "route": "/security/reachability", "title": "Reachability Analysis", "body": "Stella traces whether vulnerable code is actually callable from your app''s entry points. This separates real risks from theoretical noise — often reducing actionable findings by 60-80%."}, + {"stepOrder": 3, "route": "/security/findings", "title": "Findings & Evidence", "body": "Scan results are matched against advisory feeds (NVD, OSV). Each finding includes: severity, reachability score, VEX status, and links to evidence. Everything is cryptographically signed."}, + {"stepOrder": 4, "route": "/evidence/overview", "title": "Evidence Chain", "body": "Every scan produces signed evidence: SBOM, findings, reachability proof, and policy evaluation. This evidence chain is immutable and can be verified offline."} + ]'::jsonb, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tours_key_locale DO NOTHING; + +-- Tour 3: Triage 101 (4 steps) +INSERT INTO platform.assistant_tours (tour_key, locale, title, description, steps, tenant_id) +VALUES ('triage-101', 'en-US', 'Vulnerability Triage Workflow', + 'Learn how to efficiently triage vulnerability findings and make evidence-backed decisions.', + '[ + {"stepOrder": 0, "route": "/triage/artifacts", "title": "Select an Artifact", "body": "Each row is a scanned container image. The badge shows how many findings need attention. Click an artifact to open its triage workspace."}, + {"stepOrder": 1, "route": "/triage/artifacts", "title": "Review by Severity", "body": "Start with Critical findings — these are remotely exploitable. Then High (fix within days), Medium (next sprint), Low (track). The reachability column tells you which are actually dangerous."}, + {"stepOrder": 2, "route": "/ops/policy/vex", "title": "Create VEX Statements", "body": "For findings that don''t affect your software, create a VEX statement explaining why. ''Not Affected'' with justification suppresses the finding in future scans and reduces noise."}, + {"stepOrder": 3, "route": "/evidence/capsules", "title": "Decision Capsules", "body": "After triage, all your decisions are bundled into a Decision Capsule — signed proof of what you evaluated, what you decided, and why. Hand this to auditors."} + ]'::jsonb, '_system') +ON CONFLICT ON CONSTRAINT ux_assistant_tours_key_locale DO NOTHING; diff --git a/src/Web/StellaOps.Web/e2e/stella-assistant.e2e.spec.ts b/src/Web/StellaOps.Web/e2e/stella-assistant.e2e.spec.ts new file mode 100644 index 000000000..2e5b73ab3 --- /dev/null +++ b/src/Web/StellaOps.Web/e2e/stella-assistant.e2e.spec.ts @@ -0,0 +1,799 @@ +/** + * Stella Assistant — Black-Box E2E Tests + * + * QA perspective: tests observable behavior only. No knowledge of internal + * implementation, signals, services, or component structure. + * + * Persona: "Alex" — mid-level developer, new DevOps, first time using Stella Ops. + * Tests verify that Alex can discover, use, and benefit from the assistant. + * + * Coverage: + * 1. Mascot visibility & first impression + * 2. Tips mode — contextual help per page + * 3. Search mode — type-to-search, results, navigation + * 4. Chat mode — ask questions, get answers + * 5. Mode transitions — tips ↔ search ↔ chat + * 6. Dismiss / restore behavior + * 7. Persistence across page navigations + * 8. Tour engine — guided walkthrough + * 9. Admin editor (requires admin role) + * 10. Glossary tooltips + * 11. Context-driven tips (reacts to page state) + * 12. Accessibility (keyboard, ARIA) + * 13. Responsive (mobile viewport) + */ + +import { test, expect } from './fixtures/auth.fixture'; + +const SCREENSHOT_DIR = 'e2e/screenshots/stella-assistant'; + +async function snap(page: import('@playwright/test').Page, label: string) { + await page.screenshot({ path: `${SCREENSHOT_DIR}/${label}.png`, fullPage: false }); +} + +async function go(page: import('@playwright/test').Page, path: string) { + await page.goto(path, { waitUntil: 'networkidle', timeout: 30_000 }); + await page.waitForLoadState('domcontentloaded'); + await page.waitForTimeout(1500); +} + +// ═══════════════════════════════════════════════════════════════════════════ +// 1. MASCOT VISIBILITY & FIRST IMPRESSION +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('1 — Mascot Visibility', () => { + test('1.1 mascot appears on dashboard after login', async ({ authenticatedPage: page }) => { + await go(page, '/'); + + // The mascot should be visible in the bottom-right corner + const mascot = page.getByRole('button', { name: /stella helper/i }); + await expect(mascot).toBeVisible({ timeout: 10_000 }); + await snap(page, '01-mascot-visible'); + }); + + test('1.2 mascot has recognizable avatar image', async ({ authenticatedPage: page }) => { + await go(page, '/'); + + const mascotImg = page.locator('img[alt*="Stella"]').last(); + await expect(mascotImg).toBeVisible(); + // Should have a non-zero rendered size + const box = await mascotImg.boundingBox(); + expect(box).not.toBeNull(); + expect(box!.width).toBeGreaterThan(20); + expect(box!.height).toBeGreaterThan(20); + }); + + test('1.3 mascot is present on every authenticated page', async ({ authenticatedPage: page }) => { + const routes = [ + '/', + '/releases', + '/security', + '/evidence/overview', + '/ops/operations', + '/setup/integrations', + '/ops/policy/vex', + ]; + + for (const route of routes) { + await go(page, route); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await expect(mascot).toBeVisible({ timeout: 5000 }); + } + }); + + test('1.4 mascot auto-opens bubble on first visit to a page', async ({ authenticatedPage: page }) => { + // Clear any stored state to simulate first visit + await page.evaluate(() => { + localStorage.removeItem('stellaops.assistant.state'); + localStorage.removeItem('stellaops.helper.preferences'); + }); + + await go(page, '/'); + await page.waitForTimeout(2000); + + // Bubble should auto-open with a tip + const bubble = page.locator('[role="status"]').first(); + await expect(bubble).toBeVisible({ timeout: 5000 }); + await snap(page, '01-auto-open-first-visit'); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 2. TIPS MODE — Contextual help per page +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('2 — Tips Mode', () => { + test('2.1 clicking mascot opens tip bubble', async ({ authenticatedPage: page }) => { + await go(page, '/'); + + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Bubble should be visible with tip content + const bubble = page.locator('[role="status"]'); + await expect(bubble).toBeVisible(); + + // Should contain a title (bold text about the current page) + const tipText = await bubble.innerText(); + expect(tipText.length).toBeGreaterThan(20); + await snap(page, '02-tip-bubble-open'); + }); + + test('2.2 dashboard tips mention dashboard concepts', async ({ authenticatedPage: page }) => { + await go(page, '/'); + + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const tipText = await page.locator('[role="status"]').innerText(); + // Dashboard tips should reference dashboard-related concepts + const dashboardTerms = ['dashboard', 'overview', 'environment', 'sbom', 'vulnerability', 'feed', 'status']; + const hasRelevantTerm = dashboardTerms.some(term => + tipText.toLowerCase().includes(term) + ); + expect(hasRelevantTerm, `Tip should mention a dashboard concept. Got: "${tipText.slice(0, 100)}"`).toBe(true); + }); + + test('2.3 tips change when navigating to a different page', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const dashboardTip = await page.locator('[role="status"]').innerText(); + + // Navigate to VEX page + await go(page, '/ops/policy/vex'); + await mascot.click(); + await page.waitForTimeout(500); + + const vexTip = await page.locator('[role="status"]').innerText(); + + // Tips should be different + expect(vexTip).not.toEqual(dashboardTip); + // VEX tip should mention VEX-related concepts + const vexTerms = ['vex', 'vulnerability', 'exploitability', 'statement', 'affected']; + const hasVexTerm = vexTerms.some(term => vexTip.toLowerCase().includes(term)); + expect(hasVexTerm, `VEX tip should mention VEX concepts. Got: "${vexTip.slice(0, 100)}"`).toBe(true); + }); + + test('2.4 tip navigation (prev/next) cycles through tips', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Should show tip counter "1 / N" + const counter = page.locator('text=/\\d+ \\/ \\d+/'); + await expect(counter).toBeVisible(); + const counterText = await counter.innerText(); + expect(counterText).toMatch(/1 \/ \d+/); + + // Get first tip title + const firstTip = await page.locator('[role="status"]').innerText(); + + // Click Next + const nextBtn = page.getByRole('button', { name: /next tip/i }); + await expect(nextBtn).toBeEnabled(); + await nextBtn.click(); + await page.waitForTimeout(300); + + // Counter should update + const newCounter = await counter.innerText(); + expect(newCounter).toMatch(/2 \/ \d+/); + + // Tip content should change + const secondTip = await page.locator('[role="status"]').innerText(); + expect(secondTip).not.toEqual(firstTip); + + // Click Prev + const prevBtn = page.getByRole('button', { name: /previous tip/i }); + await expect(prevBtn).toBeEnabled(); + await prevBtn.click(); + await page.waitForTimeout(300); + + const backToFirst = await counter.innerText(); + expect(backToFirst).toMatch(/1 \/ \d+/); + }); + + test('2.5 tips with action buttons navigate to target', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Find and click through tips until we find one with an action button + const nextBtn = page.getByRole('button', { name: /next tip/i }); + for (let i = 0; i < 7; i++) { + const actionBtn = page.locator('[role="status"] button').filter({ hasText: /scan|diagnos|check|view|open/i }).first(); + if (await actionBtn.isVisible({ timeout: 500 }).catch(() => false)) { + const btnText = await actionBtn.innerText(); + await actionBtn.click(); + await page.waitForTimeout(2000); + // Should have navigated away from dashboard + const url = page.url(); + expect(url).not.toBe('/'); + await snap(page, '02-action-button-navigated'); + return; + } + if (await nextBtn.isEnabled()) { + await nextBtn.click(); + await page.waitForTimeout(300); + } + } + // If no action button found in 7 tips, that's acceptable but notable + }); + + test('2.6 close button dismisses the bubble', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const closeBtn = page.getByRole('button', { name: /close tip/i }); + await expect(closeBtn).toBeVisible(); + await closeBtn.click(); + await page.waitForTimeout(300); + + // Bubble should be gone + const bubble = page.locator('[role="status"]'); + await expect(bubble).not.toBeVisible(); + + // Mascot should still be visible + await expect(mascot).toBeVisible(); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 3. SEARCH MODE — Type to search +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('3 — Search Mode', () => { + test('3.1 search input is visible in the bubble', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const searchInput = page.getByPlaceholder(/search|ask stella/i); + await expect(searchInput).toBeVisible(); + }); + + test('3.2 typing a keyword switches to search mode', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const input = page.getByPlaceholder(/search|ask stella/i); + await input.fill('SBOM vulnerability'); + await input.press('Enter'); + await page.waitForTimeout(1000); + + // Should show "Search" in the header or a back-to-tips button + const searchHeader = page.locator('text=/Search/i').first(); + const backBtn = page.locator('text=/Tips/i').first(); + const isSearchMode = await searchHeader.isVisible({ timeout: 2000 }).catch(() => false) + || await backBtn.isVisible({ timeout: 2000 }).catch(() => false); + expect(isSearchMode).toBe(true); + await snap(page, '03-search-mode'); + }); + + test('3.3 back-to-tips button returns to tips mode', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const input = page.getByPlaceholder(/search|ask stella/i); + await input.fill('policy'); + await input.press('Enter'); + await page.waitForTimeout(1000); + + // Click back to tips + const backBtn = page.locator('button').filter({ hasText: /tips/i }).first(); + if (await backBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await backBtn.click(); + await page.waitForTimeout(500); + + // Should show tip content again (not search) + const counter = page.locator('text=/\\d+ \\/ \\d+/'); + await expect(counter).toBeVisible({ timeout: 3000 }); + } + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 4. CHAT MODE — Ask Stella questions +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('4 — Chat Mode', () => { + test('4.1 "Ask Stella" button switches to chat mode', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const askBtn = page.getByRole('button', { name: /ask stella/i }); + if (await askBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await askBtn.click(); + await page.waitForTimeout(500); + + // Should show chat header + const chatIndicator = page.locator('text=/Ask Stella/i').first(); + await expect(chatIndicator).toBeVisible({ timeout: 3000 }); + + // Input placeholder should change + const input = page.getByPlaceholder(/ask stella a question/i); + await expect(input).toBeVisible(); + await snap(page, '04-chat-mode'); + } + }); + + test('4.2 question in input auto-routes to chat mode', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const input = page.getByPlaceholder(/search|ask stella/i); + await input.fill('How do I scan my first container image?'); + await input.press('Enter'); + await page.waitForTimeout(2000); + + // Questions should trigger chat mode (not search) + const chatIndicator = page.locator('text=/Ask Stella/i').first(); + const isChat = await chatIndicator.isVisible({ timeout: 3000 }).catch(() => false); + // Even if Advisory AI service is down, the mode should switch + expect(isChat).toBe(true); + await snap(page, '04-question-to-chat'); + }); + + test('4.3 chat shows error gracefully when AI service unavailable', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const askBtn = page.getByRole('button', { name: /ask stella/i }); + if (await askBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await askBtn.click(); + await page.waitForTimeout(500); + + const input = page.getByPlaceholder(/ask stella/i); + await input.fill('What is SBOM?'); + await input.press('Enter'); + await page.waitForTimeout(3000); + + // Should show either a response or a graceful error — NOT a blank screen + const bubble = page.locator('[role="status"]'); + const bubbleText = await bubble.innerText(); + expect(bubbleText.length).toBeGreaterThan(10); + await snap(page, '04-chat-error-graceful'); + } + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 5. MODE TRANSITIONS +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('5 — Mode Transitions', () => { + test('5.1 tips → search → tips cycle works', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Verify tips mode (counter visible) + const counter = page.locator('text=/\\d+ \\/ \\d+/'); + await expect(counter).toBeVisible({ timeout: 3000 }); + + // Switch to search + const input = page.getByPlaceholder(/search|ask stella/i); + await input.fill('policy'); + await input.press('Enter'); + await page.waitForTimeout(1000); + + // Switch back to tips + const backBtn = page.locator('button').filter({ hasText: /tips/i }).first(); + if (await backBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await backBtn.click(); + await page.waitForTimeout(500); + await expect(counter).toBeVisible({ timeout: 3000 }); + } + }); + + test('5.2 navigating to new page resets to tips mode', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Enter search mode + const input = page.getByPlaceholder(/search|ask stella/i); + await input.fill('test'); + await input.press('Enter'); + await page.waitForTimeout(500); + + // Navigate to different page + await go(page, '/security'); + await mascot.click(); + await page.waitForTimeout(500); + + // Should be back in tips mode with security-related content + const tipText = await page.locator('[role="status"]').innerText(); + const securityTerms = ['security', 'posture', 'scan', 'vulnerability', 'risk']; + const hasSecTerm = securityTerms.some(t => tipText.toLowerCase().includes(t)); + expect(hasSecTerm).toBe(true); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 6. DISMISS / RESTORE +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('6 — Dismiss & Restore', () => { + test('6.1 "Don\'t show again" hides the mascot', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const dismissBtn = page.getByRole('button', { name: /don.t show again/i }); + await expect(dismissBtn).toBeVisible(); + await dismissBtn.click(); + await page.waitForTimeout(500); + + // Full mascot should be hidden + await expect(mascot).not.toBeVisible({ timeout: 2000 }); + await snap(page, '06-dismissed'); + }); + + test('6.2 restore button brings mascot back after dismiss', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Dismiss + await page.getByRole('button', { name: /don.t show again/i }).click(); + await page.waitForTimeout(500); + + // Look for restore button (small icon with "?" badge) + const restoreBtn = page.getByRole('button', { name: /show stella|bring back/i }); + await expect(restoreBtn).toBeVisible({ timeout: 3000 }); + await restoreBtn.click(); + await page.waitForTimeout(1000); + + // Mascot should be back + await expect(page.getByRole('button', { name: /stella helper/i })).toBeVisible(); + await snap(page, '06-restored'); + }); + + test('6.3 dismiss state persists across page reload', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + await page.getByRole('button', { name: /don.t show again/i }).click(); + await page.waitForTimeout(500); + + // Reload page + await page.reload({ waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + + // Mascot should still be dismissed (restore button visible instead) + await expect(mascot).not.toBeVisible({ timeout: 3000 }); + const restoreBtn = page.getByRole('button', { name: /show stella|bring back/i }); + await expect(restoreBtn).toBeVisible(); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 7. PERSISTENCE ACROSS NAVIGATION +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('7 — Persistence', () => { + test('7.1 tip position is remembered per page', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Advance to tip 3 + const nextBtn = page.getByRole('button', { name: /next tip/i }); + await nextBtn.click(); + await page.waitForTimeout(200); + await nextBtn.click(); + await page.waitForTimeout(200); + + const counter = page.locator('text=/\\d+ \\/ \\d+/'); + const pos = await counter.innerText(); + expect(pos).toMatch(/3 \/ \d+/); + + // Navigate away and back + await go(page, '/security'); + await page.waitForTimeout(500); + await go(page, '/'); + await mascot.click(); + await page.waitForTimeout(500); + + // Should return to tip 3 + const restoredPos = await counter.innerText(); + expect(restoredPos).toMatch(/3 \/ \d+/); + }); + + test('7.2 first-visit auto-open only happens once per page', async ({ authenticatedPage: page }) => { + await page.evaluate(() => { + localStorage.removeItem('stellaops.assistant.state'); + localStorage.removeItem('stellaops.helper.preferences'); + }); + + await go(page, '/'); + await page.waitForTimeout(2000); + + // First visit: bubble auto-opens + let bubble = page.locator('[role="status"]'); + const autoOpened = await bubble.isVisible({ timeout: 3000 }).catch(() => false); + expect(autoOpened).toBe(true); + + // Close it + const closeBtn = page.getByRole('button', { name: /close tip/i }); + if (await closeBtn.isVisible({ timeout: 1000 }).catch(() => false)) { + await closeBtn.click(); + } + + // Navigate away and back + await go(page, '/security'); + await go(page, '/'); + await page.waitForTimeout(2000); + + // Second visit to same page: should NOT auto-open + bubble = page.locator('[role="status"]'); + const autoOpenedAgain = await bubble.isVisible({ timeout: 2000 }).catch(() => false); + expect(autoOpenedAgain).toBe(false); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 8. CONTEXTUAL TIP RELEVANCE (per page/tab) +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('8 — Contextual Relevance', () => { + const pageExpectations: { route: string; name: string; expectedTerms: string[] }[] = [ + { route: '/', name: 'Dashboard', expectedTerms: ['dashboard', 'overview', 'environment', 'sbom', 'feed'] }, + { route: '/releases', name: 'Releases', expectedTerms: ['release', 'gate', 'evidence', 'promote', 'digest'] }, + { route: '/releases/deployments', name: 'Deployments', expectedTerms: ['deployment', 'gate', 'approval', 'promote'] }, + { route: '/security', name: 'Security Posture', expectedTerms: ['security', 'risk', 'posture', 'vex', 'sbom'] }, + { route: '/security/reachability', name: 'Reachability', expectedTerms: ['reachability', 'call', 'static', 'runtime', 'code'] }, + { route: '/ops/policy/vex', name: 'VEX', expectedTerms: ['vex', 'vulnerability', 'exploitability', 'statement', 'affected'] }, + { route: '/ops/policy/governance', name: 'Governance', expectedTerms: ['budget', 'risk', 'threshold', 'policy'] }, + { route: '/evidence/overview', name: 'Evidence', expectedTerms: ['evidence', 'signed', 'proof', 'audit', 'capsule'] }, + { route: '/ops/operations', name: 'Operations', expectedTerms: ['operations', 'blocking', 'health', 'action'] }, + { route: '/ops/operations/doctor', name: 'Diagnostics', expectedTerms: ['diagnostic', 'health', 'check', 'service'] }, + { route: '/setup/integrations', name: 'Integrations', expectedTerms: ['integration', 'registry', 'connect', 'setup'] }, + { route: '/setup/trust-signing', name: 'Certificates', expectedTerms: ['signing', 'key', 'certificate', 'trust', 'crypto'] }, + ]; + + for (const { route, name, expectedTerms } of pageExpectations) { + test(`8.x tips for ${name} (${route}) are relevant`, async ({ authenticatedPage: page }) => { + await go(page, route); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Collect all tip text (cycle through all tips) + let allTipText = ''; + const counter = page.locator('text=/\\d+ \\/ \\d+/'); + const nextBtn = page.getByRole('button', { name: /next tip/i }); + + const bubble = page.locator('[role="status"]'); + allTipText += await bubble.innerText(); + + // Cycle through remaining tips + for (let i = 0; i < 10; i++) { + if (await nextBtn.isEnabled()) { + await nextBtn.click(); + await page.waitForTimeout(200); + allTipText += ' ' + await bubble.innerText(); + } else { + break; + } + } + + // At least one expected term should appear in the combined tip text + const lowerText = allTipText.toLowerCase(); + const matchedTerms = expectedTerms.filter(t => lowerText.includes(t)); + expect( + matchedTerms.length, + `Tips for ${name} should mention at least one of: [${expectedTerms.join(', ')}]. Got text: "${allTipText.slice(0, 200)}..."` + ).toBeGreaterThan(0); + }); + } +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 9. SEND BUTTON & INPUT BEHAVIOR +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('9 — Input Behavior', () => { + test('9.1 send button is disabled when input is empty', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const sendBtn = page.getByRole('button', { name: /send/i }); + await expect(sendBtn).toBeDisabled(); + }); + + test('9.2 send button enables when text is entered', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const input = page.getByPlaceholder(/search|ask stella/i); + await input.fill('test query'); + + const sendBtn = page.getByRole('button', { name: /send/i }); + await expect(sendBtn).toBeEnabled(); + }); + + test('9.3 Enter key submits the input', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + const input = page.getByPlaceholder(/search|ask stella/i); + await input.fill('SBOM'); + await input.press('Enter'); + await page.waitForTimeout(1000); + + // Should have transitioned away from tips mode + // (either search mode header or chat mode header visible) + const modeIndicator = page.locator('text=/Search|Ask Stella/i').first(); + await expect(modeIndicator).toBeVisible({ timeout: 3000 }); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 10. ACCESSIBILITY +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('10 — Accessibility', () => { + test('10.1 mascot has accessible role and label', async ({ authenticatedPage: page }) => { + await go(page, '/'); + + // The mascot container should have complementary role + const assistant = page.getByRole('complementary', { name: /stella/i }); + await expect(assistant).toBeVisible({ timeout: 5000 }); + + // The mascot button should have aria-label + const mascot = page.getByRole('button', { name: /stella helper/i }); + await expect(mascot).toBeVisible(); + }); + + test('10.2 bubble uses aria-live for screen readers', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + // Content area should use role="status" (implicit aria-live="polite") + const liveRegion = page.locator('[role="status"]'); + await expect(liveRegion).toBeVisible(); + }); + + test('10.3 close and navigation buttons have accessible labels', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + await mascot.click(); + await page.waitForTimeout(500); + + await expect(page.getByRole('button', { name: /close tip/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /previous tip/i })).toBeVisible(); + await expect(page.getByRole('button', { name: /next tip/i })).toBeVisible(); + }); + + test('10.4 mascot button indicates expanded state', async ({ authenticatedPage: page }) => { + await go(page, '/'); + const mascot = page.getByRole('button', { name: /stella helper/i }); + + // Before clicking — not expanded + await mascot.click(); + await page.waitForTimeout(500); + + // After clicking — should have aria-expanded="true" + const expanded = await mascot.getAttribute('aria-expanded'); + expect(expanded).toBe('true'); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 11. MULTIPLE PAGES SMOKE — No crashes +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('11 — Smoke: Assistant survives rapid navigation', () => { + test('11.1 rapid navigation does not crash the assistant', async ({ authenticatedPage: page }) => { + const errors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error' && /NG0|TypeError|ReferenceError/.test(msg.text())) { + errors.push(msg.text()); + } + }); + + const routes = [ + '/', '/releases', '/releases/deployments', '/environments/overview', + '/security', '/security/reachability', '/ops/policy/vex', + '/evidence/overview', '/ops/operations', '/setup/integrations', + '/ops/policy/governance', '/ops/operations/doctor', + ]; + + for (const route of routes) { + await page.goto(route, { waitUntil: 'domcontentloaded', timeout: 15_000 }); + await page.waitForTimeout(800); + } + + // Mascot should still be functional + const mascot = page.getByRole('button', { name: /stella helper/i }); + await expect(mascot).toBeVisible(); + await mascot.click(); + await page.waitForTimeout(500); + + const bubble = page.locator('[role="status"]'); + await expect(bubble).toBeVisible(); + + // No critical Angular errors + const criticalErrors = errors.filter(e => /NG0/.test(e)); + expect(criticalErrors, `Angular errors: ${criticalErrors.join('\n')}`).toHaveLength(0); + }); +}); + +// ═══════════════════════════════════════════════════════════════════════════ +// 12. ADMIN EDITOR (requires ui.admin scope) +// ═══════════════════════════════════════════════════════════════════════════ + +test.describe('12 — Admin Editor', () => { + test('12.1 admin page renders at /console-admin/assistant', async ({ authenticatedPage: page }) => { + await go(page, '/console-admin/assistant'); + + const heading = page.locator('text=/Stella Assistant Editor/i'); + await expect(heading).toBeVisible({ timeout: 10_000 }); + await snap(page, '12-admin-editor'); + }); + + test('12.2 admin page has tabs for Tips and Glossary', async ({ authenticatedPage: page }) => { + await go(page, '/console-admin/assistant'); + await page.waitForTimeout(1000); + + const tipsTab = page.getByRole('tab', { name: /tips/i }); + const glossaryTab = page.getByRole('tab', { name: /glossary/i }); + + await expect(tipsTab).toBeVisible(); + await expect(glossaryTab).toBeVisible(); + }); + + test('12.3 new tip form has preview', async ({ authenticatedPage: page }) => { + await go(page, '/console-admin/assistant'); + await page.waitForTimeout(1000); + + const newTipTab = page.getByRole('tab', { name: /new tip/i }); + await newTipTab.click(); + await page.waitForTimeout(500); + + // Form fields should be visible + const routeInput = page.getByPlaceholder(/\/ops\/policy/i); + await expect(routeInput).toBeVisible(); + + // Preview section should exist + const preview = page.locator('text=/preview/i').first(); + await expect(preview).toBeVisible(); + await snap(page, '12-admin-new-tip-form'); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/api/search.models.ts b/src/Web/StellaOps.Web/src/app/core/api/search.models.ts index c15f9f992..8dc3c6767 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/search.models.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/search.models.ts @@ -122,6 +122,7 @@ export const SEVERITY_COLORS: Record = { }; export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ + // ── Top 5 (shown by default when palette opens) ────────────────── { id: 'scan', label: 'Scan Artifact', @@ -129,34 +130,7 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ description: 'Opens artifact scan dialog', icon: 'scan', route: '/security/scan', - keywords: ['scan', 'artifact', 'analyze'], - }, - { - id: 'vex', - label: 'Create VEX Statement', - shortcut: '>vex', - description: 'Open VEX creation workflow', - icon: 'shield-check', - route: '/security/advisories-vex', - keywords: ['vex', 'create', 'statement'], - }, - { - id: 'policy', - label: 'New Policy Pack', - shortcut: '>policy', - description: 'Create new policy pack', - icon: 'shield', - route: '/ops/policy/packs', - keywords: ['policy', 'new', 'pack', 'create'], - }, - { - id: 'jobs', - label: 'View Jobs', - shortcut: '>jobs', - description: 'Navigate to job list', - icon: 'workflow', - route: '/ops/operations/jobs-queues', - keywords: ['jobs', 'jobengine', 'list'], + keywords: ['scan', 'artifact', 'analyze', 'image', 'container', 'vulnerability'], }, { id: 'findings', @@ -165,25 +139,62 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ description: 'Navigate to findings list', icon: 'alert-triangle', route: '/security/triage', - keywords: ['findings', 'vulnerabilities', 'list'], + keywords: ['findings', 'vulnerabilities', 'list', 'triage', 'security'], }, { - id: 'settings', - label: 'Go to Settings', - shortcut: '>settings', - description: 'Navigate to settings', - icon: 'settings', - route: '/setup', - keywords: ['settings', 'config', 'preferences'], + id: 'pending-approvals', + label: 'Pending Approvals', + shortcut: '>approvals', + description: 'View pending release approvals', + icon: 'check-circle', + route: '/releases/approvals', + keywords: ['approvals', 'pending', 'gate', 'release', 'review', 'decision'], }, { - id: 'health', - label: 'Platform Health', - shortcut: '>health', - description: 'View platform health status', - icon: 'heart-pulse', - route: '/ops/operations/system-health', - keywords: ['health', 'status', 'platform', 'ops', 'doctor', 'system'], + id: 'security-posture', + label: 'Security Posture', + shortcut: '>posture', + description: 'View security posture overview', + icon: 'shield', + route: '/security', + keywords: ['security', 'posture', 'risk', 'dashboard', 'overview'], + }, + { + id: 'create-release', + label: 'Create Release', + shortcut: '>release', + description: 'Create a new release version', + icon: 'package', + route: '/releases/versions/new', + keywords: ['create', 'release', 'version', 'new', 'deploy'], + }, + // ── Remaining actions (alphabetical by id) ─────────────────────── + { + id: 'check-policy-gates', + label: 'Check Policy Gates', + shortcut: '>gates', + description: 'Review policy gate status and results', + icon: 'shield', + route: '/ops/policy/gates', + keywords: ['policy', 'gates', 'check', 'governance', 'compliance'], + }, + { + id: 'configure-advisory-sources', + label: 'Configure Advisory Sources', + shortcut: '>advisory', + description: 'Manage advisory and VEX data sources', + icon: 'plug', + route: '/setup/integrations/advisory-vex-sources', + keywords: ['advisory', 'sources', 'vex', 'configure', 'integrations', 'feed'], + }, + { + id: 'create-exception', + label: 'Create Exception', + shortcut: '>exception', + description: 'Create a new policy exception or waiver', + icon: 'file-text', + route: '/exceptions', + keywords: ['create', 'exception', 'waiver', 'exemption', 'new'], }, { id: 'doctor-quick', @@ -199,7 +210,43 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ shortcut: '>diagnostics', description: 'Run comprehensive Doctor diagnostics', icon: 'search', - keywords: ['doctor', 'diagnostics', 'full', 'comprehensive'], + keywords: ['doctor', 'diagnostics', 'full', 'comprehensive', 'diag'], + }, + { + id: 'environments', + label: 'Environments', + shortcut: '>envs', + description: 'View environment topology and targets', + icon: 'globe', + route: '/environments/overview', + keywords: ['environments', 'topology', 'regions', 'overview', 'targets', 'envs'], + }, + { + id: 'evidence-export', + label: 'Export Evidence', + shortcut: '>export', + description: 'Export evidence bundles for compliance', + icon: 'download', + route: '/evidence/exports', + keywords: ['evidence', 'export', 'bundle', 'compliance', 'report'], + }, + { + id: 'exception-queue', + label: 'Exception Queue', + shortcut: '>exceptions', + description: 'View and manage policy exceptions', + icon: 'file-text', + route: '/ops/policy/vex/exceptions', + keywords: ['exceptions', 'queue', 'waiver', 'policy', 'exemption'], + }, + { + id: 'health', + label: 'Platform Health', + shortcut: '>health', + description: 'View platform health status', + icon: 'heart-pulse', + route: '/ops/operations/system-health', + keywords: ['health', 'status', 'platform', 'ops', 'doctor', 'system'], }, { id: 'integrations', @@ -210,6 +257,42 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ route: '/ops/integrations', keywords: ['integrations', 'connect', 'manage'], }, + { + id: 'jobs', + label: 'View Jobs', + shortcut: '>jobs', + description: 'Navigate to job list', + icon: 'workflow', + route: '/ops/operations/jobs-queues', + keywords: ['jobs', 'jobengine', 'list', 'queue'], + }, + { + id: 'policy', + label: 'New Policy Pack', + shortcut: '>policy', + description: 'Create new policy pack', + icon: 'shield', + route: '/ops/policy/packs', + keywords: ['policy', 'new', 'pack', 'create'], + }, + { + id: 'reachability', + label: 'Reachability Analysis', + shortcut: '>reach', + description: 'View reachability analysis and exploitability', + icon: 'cpu', + route: '/security/reachability', + keywords: ['reachability', 'analysis', 'exploitable', 'path', 'reach'], + }, + { + id: 'risk-budget', + label: 'Risk Budget', + shortcut: '>budget', + description: 'View risk budget and governance dashboard', + icon: 'shield', + route: '/ops/policy/governance', + keywords: ['risk', 'budget', 'governance', 'threshold', 'consumption'], + }, { id: 'seed-demo', label: 'Seed Demo Data', @@ -219,49 +302,22 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ keywords: ['seed', 'demo', 'data', 'populate', 'sample', 'mock'], }, { - id: 'scan-image', - label: 'Scan Image', - shortcut: '>scan-image', - description: 'Scan a container image for vulnerabilities', - icon: 'scan', - route: '/security/scan', - keywords: ['scan', 'image', 'container', 'vulnerability'], + id: 'settings', + label: 'Go to Settings', + shortcut: '>settings', + description: 'Navigate to settings', + icon: 'settings', + route: '/setup', + keywords: ['settings', 'config', 'preferences'], }, { - id: 'view-vulnerabilities', - label: 'View Vulnerabilities', - shortcut: '>vulns', - description: 'Browse vulnerability triage queue', - icon: 'alert-triangle', - route: '/triage/artifacts', - keywords: ['vulnerabilities', 'vulns', 'triage', 'cve', 'security'], - }, - { - id: 'search-cve', - label: 'Search CVE', - shortcut: '>cve', - description: 'Search for a specific CVE in triage artifacts', - icon: 'search', - route: '/triage/artifacts', - keywords: ['cve', 'search', 'vulnerability', 'advisory'], - }, - { - id: 'view-findings', - label: 'View Findings', - shortcut: '>view-findings', - description: 'Navigate to security findings list', - icon: 'alert-triangle', - route: '/triage/artifacts', - keywords: ['findings', 'vulnerabilities', 'security', 'list'], - }, - { - id: 'create-release', - label: 'Create Release', - shortcut: '>release', - description: 'Create a new release version', - icon: 'package', - route: '/releases/versions/new', - keywords: ['create', 'release', 'version', 'new', 'deploy'], + id: 'vex', + label: 'Create VEX Statement', + shortcut: '>vex', + description: 'Open VEX creation workflow', + icon: 'shield-check', + route: '/security/advisories-vex', + keywords: ['vex', 'create', 'statement'], }, { id: 'view-audit-log', @@ -272,24 +328,6 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ route: '/evidence/audit-log', keywords: ['audit', 'log', 'evidence', 'history', 'trail'], }, - { - id: 'run-diagnostics', - label: 'Run Diagnostics', - shortcut: '>diag', - description: 'Open the Doctor diagnostics dashboard', - icon: 'activity', - route: '/ops/operations/doctor', - keywords: ['diagnostics', 'doctor', 'health', 'check', 'run'], - }, - { - id: 'configure-advisory-sources', - label: 'Configure Advisory Sources', - shortcut: '>advisory', - description: 'Manage advisory and VEX data sources', - icon: 'plug', - route: '/setup/integrations/advisory-vex-sources', - keywords: ['advisory', 'sources', 'vex', 'configure', 'integrations', 'feed'], - }, { id: 'view-promotions', label: 'View Promotions', @@ -300,13 +338,13 @@ export const DEFAULT_QUICK_ACTIONS: QuickAction[] = [ keywords: ['promotions', 'promote', 'environment', 'deploy', 'release'], }, { - id: 'check-policy-gates', - label: 'Check Policy Gates', - shortcut: '>gates', - description: 'Review policy gate status and results', - icon: 'shield', - route: '/ops/policy/gates', - keywords: ['policy', 'gates', 'check', 'governance', 'compliance'], + id: 'view-vulnerabilities', + label: 'Browse Vulnerabilities', + shortcut: '>vulns', + description: 'Browse vulnerability triage queue', + icon: 'alert-triangle', + route: '/triage/artifacts', + keywords: ['vulnerabilities', 'vulns', 'triage', 'cve', 'security', 'search', 'advisory'], }, ]; diff --git a/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.component.ts b/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.component.ts index e3b14a840..0c867d57d 100644 --- a/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/advisory-ai/chat/chat.component.ts @@ -45,47 +45,7 @@ import { imports: [FormsModule, ChatMessageComponent], template: `
- -
-
-
- - - - - - -

Search assistant

- @if (conversation()) { - {{ conversation()!.conversationId.substring(0, 8) }} - } -
-
-
- @if (conversation()) { - - } - -
-
+
@@ -174,6 +134,7 @@ import { [placeholder]="inputPlaceholder()" [disabled]="isStreaming()" [(ngModel)]="inputValue" + (ngModelChange)="onChatInput()" (keydown)="handleKeydown($event)" rows="1"> + + + +
+ + +
+ + +
+ + + @if (activeTab() === 'tips') { +
+ @if (loading()) { +
Loading...
+ } @else if (filteredTips().length === 0) { +
No tips found for this locale/route.
+ } @else { + @for (tip of filteredTips(); track tip.tipId) { +
+
+ {{ tip.routePattern }} + @if (tip.contextTrigger) { + {{ tip.contextTrigger }} + } + #{{ tip.sortOrder }} + @if (!tip.isActive) { + Inactive + } +
+
{{ tip.title }}
+
{{ tip.body | slice:0:150 }}{{ tip.body.length > 150 ? '...' : '' }}
+ @if (tip.actionLabel) { +
Action: {{ tip.actionLabel }} → {{ tip.actionRoute }}
+ } + +
+ } + } +
+ } + + + @if (activeTab() === 'glossary') { +
+ +
+ + @if (showGlossaryForm()) { +
+

{{ editingGlossaryTerm() ? 'Edit Term' : 'Create Term' }}

+ + + + +
+ + +
+
+ + +
+ @if (saveSuccess()) { +
Saved successfully.
+ } +
+ } + +
+ @for (term of glossaryTerms(); track term.termId) { +
+
{{ term.term }}
+
{{ term.definition }}
+ @if (term.extendedHelp) { +
{{ term.extendedHelp | slice:0:120 }}...
+ } + +
+ } +
+ } + + + @if (activeTab() === 'tours') { +
+ +
+ + @if (showTourForm()) { +
+

{{ editingTour() ? 'Edit Tour' : 'Create Tour' }}

+ + + + + + +
+

Steps ({{ tourSteps.length }})

+ +
+ @for (step of tourSteps; track step.stepOrder; let i = $index) { +
+
+ Step {{ step.stepOrder }} + @if (tourSteps.length > 1) { + + } +
+
+ + +
+ + + +
+ } + +
+ + +
+ @if (saveSuccess()) { +
Saved successfully.
+ } +
+ } + +
+ @if (tours().length === 0) { +
No tours found for this locale.
+ } + @for (tour of tours(); track tour.tourId) { +
+
+ {{ tour.tourKey }} + {{ tour.stepCount }} steps + {{ tour.locale }} + @if (!tour.isActive) { + Inactive + } +
+
{{ tour.title }}
+
{{ tour.description }}
+ +
+ } +
+ } + + + @if (activeTab() === 'create' || editingTip()) { +
+

{{ editingTip() ? 'Edit Tip' : 'Create Tip' }}

+ + +
+ + +
+ + +
+ + +
+ + +
+
Preview
+
+ {{ formTitle || 'Tip title...' }} +

{{ formBody || 'Tip body text...' }}

+ @if (formActionLabel) { + + } +
+
+ +
+ + @if (editingTip()) { + + } +
+ @if (saveSuccess()) { +
Saved successfully.
+ } +
+ } +
+ `, + styles: [` + .admin { + max-width: 900px; + margin: 0 auto; + } + + .admin__header { + margin-bottom: 24px; + } + + .admin__title { + font-size: 1.25rem; + font-weight: 700; + color: var(--color-text-heading); + } + + .admin__subtitle { + font-size: 0.8125rem; + color: var(--color-text-secondary); + margin-top: 4px; + } + + .admin__tabs { + display: flex; + gap: 2px; + border-bottom: 2px solid var(--color-border-primary); + margin-bottom: 16px; + } + + .admin__tab { + padding: 8px 16px; + border: none; + background: transparent; + font-size: 0.8125rem; + font-weight: 600; + color: var(--color-text-secondary); + cursor: pointer; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + + &:hover { color: var(--color-text-primary); } + } + + .admin__tab--active { + color: var(--color-brand-primary); + border-bottom-color: var(--color-brand-primary); + } + + .admin__filters { + display: flex; + gap: 8px; + margin-bottom: 16px; + } + + .admin__select, .admin__input { + padding: 6px 10px; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md, 8px); + font-size: 0.75rem; + background: var(--color-surface-primary); + color: var(--color-text-primary); + } + + .admin__input { flex: 1; } + + .admin__list { + display: flex; + flex-direction: column; + gap: 8px; + } + + .admin__loading, .admin__empty { + padding: 24px; + text-align: center; + color: var(--color-text-secondary); + font-size: 0.8125rem; + } + + .admin__card { + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md, 8px); + padding: 12px; + background: var(--color-surface-primary); + } + + .admin__card--inactive { + opacity: 0.5; + } + + .admin__card-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; + } + + .admin__route { + font-size: 0.625rem; + background: var(--color-surface-tertiary); + padding: 2px 6px; + border-radius: var(--radius-sm, 4px); + } + + .admin__context-badge { + font-size: 0.5625rem; + background: var(--color-brand-primary); + color: var(--color-text-heading); + padding: 1px 6px; + border-radius: var(--radius-full, 9999px); + font-weight: 700; + } + + .admin__sort { + font-size: 0.625rem; + color: var(--color-text-secondary); + } + + .admin__inactive-badge { + font-size: 0.5625rem; + background: var(--color-status-error, #c62828); + color: white; + padding: 1px 6px; + border-radius: var(--radius-full, 9999px); + } + + .admin__card-title { + font-weight: 700; + font-size: 0.8125rem; + color: var(--color-text-heading); + margin-bottom: 4px; + } + + .admin__card-body { + font-size: 0.75rem; + color: var(--color-text-primary); + line-height: 1.5; + } + + .admin__card-body--ext { + color: var(--color-text-secondary); + font-style: italic; + margin-top: 4px; + } + + .admin__card-action { + font-size: 0.625rem; + color: var(--color-brand-primary); + margin-top: 4px; + } + + .admin__card-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 8px; + font-size: 0.625rem; + color: var(--color-text-secondary); + } + + .admin__card-actions { + display: flex; + gap: 4px; + } + + .admin__btn { + padding: 4px 12px; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md, 8px); + background: transparent; + color: var(--color-text-primary); + font-size: 0.6875rem; + font-weight: 600; + cursor: pointer; + + &:hover { border-color: var(--color-brand-primary); } + } + + .admin__btn--small { padding: 2px 8px; font-size: 0.625rem; } + + .admin__btn--primary { + background: var(--color-brand-primary); + border-color: var(--color-brand-primary); + color: var(--color-text-heading); + } + + .admin__btn--danger { + color: var(--color-status-error, #c62828); + border-color: var(--color-status-error, #c62828); + + &:hover { background: var(--color-status-error, #c62828); color: white; } + } + + .admin__form { + margin-top: 16px; + } + + .admin__label { + display: block; + font-size: 0.6875rem; + font-weight: 600; + color: var(--color-text-secondary); + margin-bottom: 12px; + + .admin__input, .admin__select, .admin__textarea { + display: block; + width: 100%; + margin-top: 4px; + } + } + + .admin__textarea { + padding: 8px 10px; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md, 8px); + font-size: 0.75rem; + background: var(--color-surface-primary); + color: var(--color-text-primary); + resize: vertical; + font-family: inherit; + } + + .admin__row { + display: flex; + gap: 12px; + + .admin__label { flex: 1; } + } + + .admin__preview { + margin: 16px 0; + padding: 12px; + border: 1px dashed var(--color-border-primary); + border-radius: var(--radius-md, 8px); + background: var(--color-surface-tertiary); + } + + .admin__preview-label { + font-size: 0.5625rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-text-secondary); + margin-bottom: 8px; + } + + .admin__preview-bubble { + font-size: 0.75rem; + line-height: 1.55; + color: var(--color-text-primary); + + strong { + display: block; + margin-bottom: 4px; + color: var(--color-text-heading); + } + + p { margin: 0; } + } + + .admin__preview-action { + margin-top: 8px; + padding: 3px 8px; + border: 1px solid var(--color-brand-primary); + border-radius: var(--radius-sm, 4px); + background: transparent; + color: var(--color-brand-primary); + font-size: 0.625rem; + font-weight: 600; + cursor: default; + } + + .admin__form-actions { + display: flex; + gap: 8px; + margin-top: 16px; + } + + .admin__success { + margin-top: 8px; + font-size: 0.75rem; + color: var(--color-status-success, #2e7d32); + font-weight: 600; + } + + .admin__section-actions { + display: flex; + justify-content: flex-end; + margin-bottom: 12px; + } + + .admin__steps-header { + display: flex; + justify-content: space-between; + align-items: center; + margin: 16px 0 8px; + + h3 { + font-size: 0.8125rem; + font-weight: 700; + color: var(--color-text-heading); + margin: 0; + } + } + + .admin__step-card { + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md, 8px); + padding: 10px; + margin-bottom: 8px; + background: var(--color-surface-tertiary); + } + + .admin__step-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + .admin__step-number { + font-size: 0.6875rem; + font-weight: 700; + color: var(--color-brand-primary); + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AssistantAdminComponent implements OnInit { + private readonly http = inject(HttpClient); + private readonly i18n = inject(I18nService); + + readonly locales = SUPPORTED_LOCALES; + + readonly activeTab = signal<'tips' | 'glossary' | 'tours' | 'create'>('tips'); + readonly tips = signal([]); + readonly glossaryTerms = signal([]); + readonly tours = signal([]); + readonly loading = signal(false); + readonly editingTip = signal(null); + readonly editingGlossaryTerm = signal(null); + readonly showGlossaryForm = signal(false); + readonly editingTour = signal(null); + readonly showTourForm = signal(false); + readonly saveSuccess = signal(false); + + selectedLocale = 'en-US'; + routeFilter = ''; + + // Form fields + formRoute = ''; + formContext = ''; + formLocale = 'en-US'; + formSort = 0; + formTitle = ''; + formBody = ''; + formActionLabel = ''; + formActionRoute = ''; + + // Glossary form fields + glossaryTerm = ''; + glossaryLocale = 'en-US'; + glossaryDefinition = ''; + glossaryExtendedHelp = ''; + glossaryRelatedTerms = ''; + glossaryRelatedRoutes = ''; + + // Tour form fields + tourKey = ''; + tourTitle = ''; + tourDescription = ''; + tourLocale = 'en-US'; + tourSteps: TourStep[] = [{ stepOrder: 1, route: '', title: '', body: '' }]; + + readonly filteredTips = computed(() => { + const all = this.tips(); + if (!this.routeFilter) return all; + return all.filter(t => t.routePattern.includes(this.routeFilter)); + }); + + ngOnInit(): void { + this.loadTips(); + } + + loadTips(): void { + this.loading.set(true); + const params: Record = { locale: this.selectedLocale }; + if (this.routeFilter) params['route'] = this.routeFilter; + + this.http.get(`${API}/admin/tips`, { params }).subscribe({ + next: (tips) => { this.tips.set(tips); this.loading.set(false); }, + error: () => { this.tips.set([]); this.loading.set(false); }, + }); + } + + loadGlossary(): void { + this.http.get<{ terms: GlossaryTermAdmin[] }>(`${API}/glossary`, { + params: { locale: this.selectedLocale }, + }).subscribe({ + next: (resp) => this.glossaryTerms.set(resp.terms), + error: () => this.glossaryTerms.set([]), + }); + } + + editTip(tip: TipAdmin): void { + this.editingTip.set(tip); + this.activeTab.set('create'); + this.formRoute = tip.routePattern; + this.formContext = tip.contextTrigger ?? ''; + this.formLocale = tip.locale; + this.formSort = tip.sortOrder; + this.formTitle = tip.title; + this.formBody = tip.body; + this.formActionLabel = tip.actionLabel ?? ''; + this.formActionRoute = tip.actionRoute ?? ''; + } + + cancelEdit(): void { + this.editingTip.set(null); + this.activeTab.set('tips'); + this.clearForm(); + } + + saveTip(): void { + const request = { + routePattern: this.formRoute, + contextTrigger: this.formContext || null, + locale: this.formLocale, + sortOrder: this.formSort, + title: this.formTitle, + body: this.formBody, + actionLabel: this.formActionLabel || null, + actionRoute: this.formActionRoute || null, + learnMoreUrl: null, + isActive: true, + productVersion: null, + }; + + this.http.post(`${API}/admin/tips`, request).subscribe({ + next: () => { + this.saveSuccess.set(true); + setTimeout(() => this.saveSuccess.set(false), 3000); + this.editingTip.set(null); + this.clearForm(); + this.loadTips(); + }, + error: () => {}, + }); + } + + deactivateTip(tipId: string): void { + this.http.delete(`${API}/admin/tips/${tipId}`).subscribe({ + next: () => this.loadTips(), + }); + } + + // ─── Glossary CRUD ────────────────────────────────────────────────── + + startCreateGlossary(): void { + this.editingGlossaryTerm.set(null); + this.clearGlossaryForm(); + this.showGlossaryForm.set(true); + } + + editGlossaryTerm(term: GlossaryTermAdmin): void { + this.editingGlossaryTerm.set(term); + this.glossaryTerm = term.term; + this.glossaryLocale = this.selectedLocale; + this.glossaryDefinition = term.definition; + this.glossaryExtendedHelp = term.extendedHelp ?? ''; + this.glossaryRelatedTerms = term.relatedTerms.join(', '); + this.glossaryRelatedRoutes = term.relatedRoutes.join(', '); + this.showGlossaryForm.set(true); + } + + cancelGlossaryEdit(): void { + this.editingGlossaryTerm.set(null); + this.showGlossaryForm.set(false); + this.clearGlossaryForm(); + } + + saveGlossaryTerm(): void { + const request = { + term: this.glossaryTerm, + locale: this.glossaryLocale, + definition: this.glossaryDefinition, + extendedHelp: this.glossaryExtendedHelp || null, + relatedTerms: this.glossaryRelatedTerms + ? this.glossaryRelatedTerms.split(',').map(s => s.trim()).filter(Boolean) + : [], + relatedRoutes: this.glossaryRelatedRoutes + ? this.glossaryRelatedRoutes.split(',').map(s => s.trim()).filter(Boolean) + : [], + isActive: true, + }; + + this.http.post(`${API}/admin/glossary`, request).subscribe({ + next: () => { + this.saveSuccess.set(true); + setTimeout(() => this.saveSuccess.set(false), 3000); + this.editingGlossaryTerm.set(null); + this.showGlossaryForm.set(false); + this.clearGlossaryForm(); + this.loadGlossary(); + }, + error: () => {}, + }); + } + + private clearGlossaryForm(): void { + this.glossaryTerm = ''; + this.glossaryLocale = this.selectedLocale; + this.glossaryDefinition = ''; + this.glossaryExtendedHelp = ''; + this.glossaryRelatedTerms = ''; + this.glossaryRelatedRoutes = ''; + } + + // ─── Tours CRUD ───────────────────────────────────────────────────── + + loadTours(): void { + this.http.get(`${API}/admin/tours`, { + params: { locale: this.selectedLocale }, + }).subscribe({ + next: (tours) => this.tours.set(tours), + error: () => this.tours.set([]), + }); + } + + startCreateTour(): void { + this.editingTour.set(null); + this.clearTourForm(); + this.showTourForm.set(true); + } + + editTour(tour: TourAdminListItem): void { + // Fetch full tour detail to get steps + this.http.get(`${API}/admin/tours/${tour.tourKey}`, { + params: { locale: this.selectedLocale }, + }).subscribe({ + next: (detail) => { + this.editingTour.set(detail); + this.tourKey = detail.tourKey; + this.tourTitle = detail.title; + this.tourDescription = detail.description; + this.tourLocale = tour.locale; + this.tourSteps = detail.steps.length > 0 + ? detail.steps.map(s => ({ ...s })) + : [{ stepOrder: 1, route: '', title: '', body: '' }]; + this.showTourForm.set(true); + }, + error: () => {}, + }); + } + + cancelTourEdit(): void { + this.editingTour.set(null); + this.showTourForm.set(false); + this.clearTourForm(); + } + + addTourStep(): void { + const nextOrder = this.tourSteps.length > 0 + ? Math.max(...this.tourSteps.map(s => s.stepOrder)) + 1 + : 1; + this.tourSteps = [...this.tourSteps, { stepOrder: nextOrder, route: '', title: '', body: '' }]; + } + + removeTourStep(index: number): void { + this.tourSteps = this.tourSteps.filter((_, i) => i !== index); + // Re-number + this.tourSteps.forEach((s, i) => s.stepOrder = i + 1); + } + + saveTour(): void { + const steps = this.tourSteps.map(s => { + const step: Record = { + stepOrder: s.stepOrder, + route: s.route, + title: s.title, + body: s.body, + }; + if (s.selector) step['selector'] = s.selector; + if (s.action) step['action'] = s.action; + return step; + }); + + const request = { + tourKey: this.tourKey, + title: this.tourTitle, + description: this.tourDescription, + locale: this.tourLocale, + steps, + isActive: true, + }; + + this.http.post(`${API}/admin/tours`, request).subscribe({ + next: () => { + this.saveSuccess.set(true); + setTimeout(() => this.saveSuccess.set(false), 3000); + this.editingTour.set(null); + this.showTourForm.set(false); + this.clearTourForm(); + this.loadTours(); + }, + error: () => {}, + }); + } + + private clearTourForm(): void { + this.tourKey = ''; + this.tourTitle = ''; + this.tourDescription = ''; + this.tourLocale = this.selectedLocale; + this.tourSteps = [{ stepOrder: 1, route: '', title: '', body: '' }]; + } + + private clearForm(): void { + this.formRoute = ''; + this.formContext = ''; + this.formLocale = this.selectedLocale; + this.formSort = 0; + this.formTitle = ''; + this.formBody = ''; + this.formActionLabel = ''; + this.formActionRoute = ''; + } +} diff --git a/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts b/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts index c40b0f7be..0da1a4b54 100644 --- a/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/app-topbar/app-topbar.component.ts @@ -18,7 +18,7 @@ import { filter } from 'rxjs/operators'; import { AuthSessionStore } from '../../core/auth/auth-session.store'; import { ConsoleSessionService } from '../../core/console/console-session.service'; import { ConsoleSessionStore } from '../../core/console/console-session.store'; -import { GlobalSearchComponent } from '../global-search/global-search.component'; +// GlobalSearchComponent removed — search is now handled by the Stella mascot import { ContextChipsComponent } from '../context-chips/context-chips.component'; import { UserMenuComponent } from '../../shared/components/user-menu/user-menu.component'; import { I18nService, UserLocalePreferenceService } from '../../core/i18n'; @@ -44,7 +44,6 @@ import { ContentWidthService } from '../../core/services/content-width.service'; selector: 'app-topbar', standalone: true, imports: [ - GlobalSearchComponent, ContextChipsComponent, UserMenuComponent, OfflineStatusChipComponent, @@ -57,8 +56,8 @@ import { ContentWidthService } from '../../core/services/content-width.service'; ], template: ` `, @@ -221,46 +205,30 @@ import { ContentWidthService } from '../../core/services/content-width.service'; /* ---- Shell ---- */ .topbar { display: flex; - flex-direction: column; background: var(--color-header-bg); border-bottom: 1px solid color-mix(in srgb, var(--color-border-primary) 35%, transparent); } - /* ---- Row layout ---- */ + /* ---- Single merged row ---- */ .topbar__row { display: flex; align-items: center; - gap: 0.75rem; - padding: 0 1rem; - } - - .topbar__row--primary { - height: 44px; - } - - .topbar__row--secondary { - height: 32px; gap: 0.5rem; padding: 0 1rem; + height: 40px; + width: 100%; overflow: visible; } @media (max-width: 575px) { .topbar__row { - gap: 0.375rem; + gap: 0.25rem; padding: 0 0.5rem; - } - - .topbar__row--secondary { - padding: 0 0.5rem; - display: none; - } - - .topbar__row--secondary-open { - display: flex; + flex-wrap: wrap; height: auto; - padding: 0.35rem 0.5rem; - overflow: visible; + min-height: 36px; + padding-top: 4px; + padding-bottom: 4px; } } @@ -293,20 +261,7 @@ import { ContentWidthService } from '../../core/services/content-width.service'; } } - /* ---- Search ---- */ - .topbar__search { - flex: 1; - max-width: 540px; - min-width: 0; - } - - @media (max-width: 575px) { - .topbar__search { - max-width: none; - } - } - - /* ---- Right section (row 1) ---- */ + /* ---- Right section ---- */ .topbar__right { display: flex; align-items: center; @@ -524,13 +479,13 @@ import { ContentWidthService } from '../../core/services/content-width.service'; white-space: nowrap; } - /* ---- Status chips (right-aligned) ---- */ + /* ---- Status chips (leading position) ---- */ .topbar__status-chips { display: flex; align-items: center; gap: 0.375rem; flex-wrap: nowrap; - margin-left: auto; + flex-shrink: 0; } /* ---- Content width toggle ---- */ diff --git a/src/Web/StellaOps.Web/src/app/layout/global-search/global-search.component.ts b/src/Web/StellaOps.Web/src/app/layout/global-search/global-search.component.ts index 482c7cb29..d27a9e802 100644 --- a/src/Web/StellaOps.Web/src/app/layout/global-search/global-search.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/global-search/global-search.component.ts @@ -33,6 +33,7 @@ import { SynthesisPanelComponent } from '../../shared/components/synthesis-panel import { AmbientContextService } from '../../core/services/ambient-context.service'; import { SearchChatContextService } from '../../core/services/search-chat-context.service'; import { SearchAssistantDrawerService } from '../../core/services/search-assistant-drawer.service'; +import { StellaAssistantService } from '../../shared/components/stella-helper/stella-assistant.service'; import { I18nService } from '../../core/i18n'; import { TelemetryClient } from '../../core/telemetry/telemetry.client'; import { normalizeSearchActionRoute } from './search-route-matrix'; @@ -975,6 +976,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy { private readonly ambientContext = inject(AmbientContextService); private readonly searchChatContext = inject(SearchChatContextService); private readonly assistantDrawer = inject(SearchAssistantDrawerService); + private readonly stellaAssistant = inject(StellaAssistantService); private readonly i18n = inject(I18nService); private readonly telemetry = inject(TelemetryClient); private readonly destroy$ = new Subject(); @@ -2321,7 +2323,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy { this.closeResults(); this.assistantDrawer.open({ initialUserMessage: suggestedPrompt, - source: 'global_search_answer', + source: 'global_search', fallbackFocusTarget: this.searchInputRef?.nativeElement ?? null, }); } @@ -2432,7 +2434,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy { this.closeResults(); this.assistantDrawer.open({ initialUserMessage: suggestedPrompt, - source: 'global_search_entry', + source: 'global_search', fallbackFocusTarget: this.searchInputRef?.nativeElement ?? null, }); } diff --git a/src/Web/StellaOps.Web/src/app/layout/search-assistant-host/search-assistant-host.component.ts b/src/Web/StellaOps.Web/src/app/layout/search-assistant-host/search-assistant-host.component.ts index 58236d3d2..792c444d3 100644 --- a/src/Web/StellaOps.Web/src/app/layout/search-assistant-host/search-assistant-host.component.ts +++ b/src/Web/StellaOps.Web/src/app/layout/search-assistant-host/search-assistant-host.component.ts @@ -6,114 +6,469 @@ import { ViewChild, effect, inject, + signal, + computed, } from '@angular/core'; +import { Router } from '@angular/router'; import { PlatformContextStore } from '../../core/context/platform-context.store'; import { SearchAssistantDrawerService } from '../../core/services/search-assistant-drawer.service'; +import { StellaAssistantService } from '../../shared/components/stella-helper/stella-assistant.service'; import { ChatComponent } from '../../features/advisory-ai/chat'; +interface TrailBubble { x: number; y: number; size: number; delay: number; } + @Component({ selector: 'app-search-assistant-host', standalone: true, imports: [ChatComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - @if (assistantDrawer.isOpen()) { + @if (assistantDrawer.isOpen() || exiting()) {
-
} `, styles: [` - .assistant-host__backdrop { - position: fixed; - inset: 0; - z-index: 185; + /* ===== Backdrop ===== */ + .ah__backdrop { + position: fixed; inset: 0; z-index: 185; background: rgba(15, 23, 42, 0.16); backdrop-filter: blur(2px); - display: flex; - align-items: flex-start; - justify-content: flex-end; - padding: 4.5rem 1rem 1rem; + display: flex; align-items: flex-start; justify-content: flex-end; + padding: 3rem 1rem 1rem; + animation: ah-fade-in 300ms ease both; } + @keyframes ah-fade-in { from { opacity: 0; } to { opacity: 1; } } + .ah--exiting { animation: ah-fade-out 250ms ease both; } + @keyframes ah-fade-out { from { opacity: 1; } to { opacity: 0; } } - .assistant-host__drawer { + /* ===== Drawer ===== */ + .ah__drawer { width: min(520px, calc(100vw - 2rem)); height: min(78vh, 760px); border: 1px solid var(--color-border-primary); - border-radius: var(--radius-lg); + border-radius: var(--radius-lg, 12px); background: var(--color-surface-primary); - box-shadow: var(--shadow-dropdown); + box-shadow: 0 8px 40px rgba(0,0,0,0.15), 0 0 0 1px rgba(245,166,35,0.08); overflow: hidden; + transform-origin: bottom right; + animation: ah-grow 400ms cubic-bezier(0.34, 1.56, 0.64, 1) both; + display: flex; flex-direction: column; + } + @keyframes ah-grow { + 0% { opacity:0; transform: scale(0.06) translateY(40vh); border-radius:50%; } + 35% { opacity:0.9; transform: scale(0.25) translateY(22vh); border-radius:50%; } + 65% { opacity:1; transform: scale(0.8) translateY(3vh); border-radius:20px; } + 100% { opacity:1; transform: scale(1) translateY(0); border-radius: var(--radius-lg, 12px); } + } + .ah--exiting .ah__drawer { + animation: ah-shrink 280ms cubic-bezier(0.36,0,0.66,-0.56) both; + } + @keyframes ah-shrink { + 0% { opacity:1; transform: scale(1) translateY(0); border-radius: var(--radius-lg, 12px); } + 100% { opacity:0; transform: scale(0.08) translateY(35vh); border-radius:50%; } } - @media (max-width: 900px) { - .assistant-host__backdrop { - padding: 0; - align-items: stretch; - } + .ah__drawer--expanded { + width: calc(100vw - 8rem); max-width: 1200px; + height: calc(100vh - 8rem); max-height: none; + transition: width 300ms cubic-bezier(0.22,1,0.36,1), height 300ms cubic-bezier(0.22,1,0.36,1); + } - .assistant-host__drawer { - width: 100vw; - height: 100vh; - border-radius: 0; + /* ===== Title bar ===== */ + .ah__titlebar { + display: flex; align-items: center; justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid var(--color-border-primary); + background: color-mix(in srgb, var(--color-surface-primary) 95%, var(--color-brand-primary)); + flex-shrink: 0; + } + .ah__titlebar-left { display: flex; align-items: center; gap: 8px; } + .ah__titlebar-icon { border-radius: 50%; } + .ah__titlebar-text { margin:0; font-size:0.8125rem; font-weight:600; color: var(--color-text-heading); } + .ah__titlebar-right { display: flex; align-items: center; gap: 4px; } + + .ah__toggle-btn { + display: inline-flex; align-items: center; gap: 5px; + padding: 4px 10px; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-full, 9999px); + background: var(--color-surface-secondary); + color: var(--color-text-secondary); + font-size: 0.625rem; font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + } + .ah__toggle-btn:hover { + border-color: var(--color-brand-primary); + color: var(--color-brand-primary); + background: color-mix(in srgb, var(--color-brand-primary) 8%, var(--color-surface-secondary)); + } + .ah__toggle-btn svg { width:14px; height:14px; flex-shrink:0; } + + .ah__hdr-btn { + width:28px; height:28px; border:none; background:transparent; + color: var(--color-text-muted); cursor:pointer; border-radius: var(--radius-md); + display:flex; align-items:center; justify-content:center; + transition: all 0.15s; + } + .ah__hdr-btn:hover { background: var(--color-surface-tertiary); color: var(--color-text-primary); } + .ah__close-btn:hover { background: color-mix(in srgb, var(--color-status-error, #c62828) 10%, transparent); color: var(--color-status-error, #c62828); } + + /* ===== Content ===== */ + .ah__content { flex:1; display:flex; flex-direction:column; min-height:0; overflow:hidden; } + .ah__content stellaops-chat { flex:1; } + + /* ===== Search view ===== */ + .ah__search-view { display:flex; flex-direction:column; height:100%; } + .ah__search-bar { + display:flex; align-items:center; gap:8px; + padding: 10px 14px; + border-bottom: 1px solid var(--color-border-primary); + flex-shrink:0; + color: var(--color-text-secondary); + } + .ah__search-input { + flex:1; border:1px solid var(--color-border-primary); + border-radius: var(--radius-full, 9999px); + background: var(--color-surface-secondary); + padding:6px 14px; font-size:0.75rem; + color: var(--color-text-primary); outline:none; min-width:0; + transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; + } + .ah__search-input::placeholder { color: var(--color-text-secondary); font-style:italic; } + .ah__search-input:focus { + border-color: var(--color-brand-primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-brand-primary) 12%, transparent); + background: var(--color-surface-primary); + } + .ah__spinner { + width:14px; height:14px; border:2px solid var(--color-border-primary); + border-top-color: var(--color-brand-primary); + border-radius:50%; animation: spin 0.6s linear infinite; flex-shrink:0; + } + @keyframes spin { to { transform: rotate(360deg); } } + + .ah__search-body { flex:1; overflow-y:auto; padding:10px; } + .ah__syn { font-size:0.75rem; line-height:1.5; color: var(--color-text-primary); padding:8px; margin-bottom:6px; border-bottom: 1px solid color-mix(in srgb, var(--color-border-primary) 25%, transparent); } + + .ah__card { + display:flex; flex-direction:column; gap:2px; + padding:8px 10px; border:1px solid transparent; + border-radius: var(--radius-md, 8px); + text-decoration:none; color:inherit; cursor:pointer; + transition: all 0.15s; + } + .ah__card:hover { background: var(--color-surface-secondary); border-color: color-mix(in srgb, var(--color-brand-primary) 20%, transparent); transform: translateX(2px); } + .ah__card-type { + font-size:0.5rem; font-weight:700; text-transform:uppercase; + letter-spacing:0.06em; color: var(--color-brand-primary); + display:inline-flex; align-items:center; gap:4px; + } + .ah__card-type::before { content:''; width:6px; height:6px; border-radius:50%; background: var(--color-brand-primary); opacity:0.5; } + .ah__card-title { font-size:0.75rem; font-weight:600; color: var(--color-text-heading); line-height:1.3; } + .ah__card-snip { font-size:0.625rem; color: var(--color-text-secondary); line-height:1.4; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; } + .ah__search-empty { padding:24px 16px; text-align:center; color: var(--color-text-secondary); font-size:0.75rem; } + + /* ===== Bubble trail ===== */ + .ah__trail { position:fixed; inset:0; z-index:190; pointer-events:none; } + .ah__bubble { + position:absolute; border-radius:50%; + background: var(--color-surface-primary, #FFFCF5); + border: 2px solid var(--color-brand-primary, #F5A623); + box-shadow: 0 1px 6px rgba(245,166,35,0.15); + opacity:0; + animation: ah-pop 280ms cubic-bezier(0.34,1.56,0.64,1) forwards; + } + @keyframes ah-pop { 0%{opacity:0;transform:scale(0)} 70%{opacity:1;transform:scale(1.15)} 100%{opacity:1;transform:scale(1)} } + + .ah__trail:not(.ah__trail--streaming) .ah__bubble { + animation: ah-pop 280ms cubic-bezier(0.34,1.56,0.64,1) forwards, ah-breathe 3s ease-in-out 700ms infinite; + } + @keyframes ah-breathe { 0%,100%{transform:scale(1);opacity:0.8} 50%{transform:scale(1.06);opacity:1} } + + .ah__trail--streaming .ah__bubble { + animation: ah-pop 280ms cubic-bezier(0.34,1.56,0.64,1) forwards, ah-pulse 0.7s ease-in-out infinite; + } + @keyframes ah-pulse { + 0%,100% { transform:scale(1); opacity:0.6; box-shadow: 0 0 4px rgba(245,166,35,0.2); } + 50% { transform:scale(1.4); opacity:1; box-shadow: 0 0 18px rgba(245,166,35,0.55); } + } + + .ah--exiting .ah__bubble { animation: ah-vanish 180ms ease forwards !important; } + @keyframes ah-vanish { to { opacity:0; transform:scale(0); } } + + /* Entrance trail pseudo-elements */ + .ah--entering .ah__backdrop::before, + .ah--entering .ah__backdrop::after { + content:''; position:fixed; border-radius:50%; + background: var(--color-brand-primary, #F5A623); + pointer-events:none; z-index:191; + } + .ah--entering .ah__backdrop::before { width:14px;height:14px; animation: ah-fly1 380ms cubic-bezier(0.18,0.89,0.32,1) both; } + .ah--entering .ah__backdrop::after { width:9px;height:9px; animation: ah-fly2 420ms cubic-bezier(0.18,0.89,0.32,1) 60ms both; } + @keyframes ah-fly1 { 0%{bottom:56px;right:80px;opacity:0.8;transform:scale(0.4)} 50%{opacity:0.6;transform:scale(1)} 100%{bottom:calc(100vh - 5rem);right:50%;opacity:0;transform:scale(0.2)} } + @keyframes ah-fly2 { 0%{bottom:50px;right:90px;opacity:0.6;transform:scale(0.3)} 60%{opacity:0.5;transform:scale(0.8)} 100%{bottom:calc(100vh - 7rem);right:45%;opacity:0;transform:scale(0.15)} } + + /* ===== Mobile ===== */ + @media (max-width: 900px) { + .ah__backdrop { padding:0; align-items:stretch; } + .ah__drawer { width:100vw; height:100vh; border-radius:0; animation: ah-mobile 250ms cubic-bezier(0.18,0.89,0.32,1) both; } + @keyframes ah-mobile { from{opacity:0;transform:scale(0.96)} to{opacity:1;transform:scale(1)} } + .ah__trail { display:none; } + .ah--entering .ah__backdrop::before, .ah--entering .ah__backdrop::after { display:none; } + } + + /* ===== Reduced motion ===== */ + @media (prefers-reduced-motion: reduce) { + .ah__drawer, .ah__bubble, .ah__backdrop, + .ah--entering .ah__backdrop::before, .ah--entering .ah__backdrop::after { + animation: none !important; opacity:1 !important; } } `], }) export class SearchAssistantHostComponent { + private readonly router = inject(Router); readonly assistantDrawer = inject(SearchAssistantDrawerService); readonly context = inject(PlatformContextStore); + readonly stella = inject(StellaAssistantService); - @ViewChild('assistantDrawerElement') private drawerRef?: ElementRef; + readonly entering = signal(false); + readonly exiting = signal(false); + readonly drawerExpanded = signal(false); + readonly chatVisible = signal(true); + readonly searchViewQuery = signal(''); + readonly trailBubbles = signal([]); + + @ViewChild('drawerEl') private drawerRef?: ElementRef; constructor() { effect(() => { - if (!this.assistantDrawer.isOpen()) { - return; + if (this.assistantDrawer.isOpen()) { + this.entering.set(true); + this.chatVisible.set(true); + setTimeout(() => { this.entering.set(false); this.computeTrail(); }, 500); + setTimeout(() => this.drawerRef?.nativeElement?.focus(), 100); + setTimeout(() => this.computeTrail(), 50); } - - setTimeout(() => this.drawerRef?.nativeElement?.focus(), 0); }); } close(): void { - this.assistantDrawer.close(); + this.exiting.set(true); + setTimeout(() => { + this.exiting.set(false); + this.trailBubbles.set([]); + this.assistantDrawer.close(); + }, 280); + } + + onToggleView(): void { this.stella.toggleDrawerView(); } + + onNewSession(): void { + this.stella.newSession(); + this.chatVisible.set(false); + setTimeout(() => this.chatVisible.set(true), 0); + } + + onToggleExpand(): void { + this.drawerExpanded.update(v => !v); + setTimeout(() => this.computeTrail(), 350); + } + + onConversationCreated(conversationId: string): void { + this.stella.setActiveConversation(conversationId); + } + + onChatTyping(value: string): void { + // Auto-derive search results from chat input (only in chat view) + if (this.stella.drawerView() === 'chat' && value.trim().length >= 2) { + this.stella.searchAsYouType(value); + } + } + + onSearchViewInput(event: Event): void { + const value = (event.target as HTMLInputElement).value; + this.searchViewQuery.set(value); + if (value.trim().length >= 2) { + this.stella.searchAsYouType(value); + } else if (!value.trim()) { + this.stella.clearSearch(); + } + } + + getCardRoute(card: any): string { + const nav = card.actions?.find((a: any) => a.actionType === 'navigate'); + return nav?.route ?? '#'; + } + + onCardClick(event: Event, card: any): void { + event.preventDefault(); + const route = this.getCardRoute(card); + if (route && route !== '#') { + this.assistantDrawer.close(); + this.trailBubbles.set([]); + this.router.navigateByUrl(route); + } } onSearchForMore(query: string): void { if (query.trim()) { - this.assistantDrawer.close({ restoreFocus: false }); + this.stella.drawerView.set('search'); + this.searchViewQuery.set(query); + this.stella.searchAsYouType(query); } } onBackdropClick(event: MouseEvent): void { - if (event.target === event.currentTarget) { - this.close(); - } + if (event.target === event.currentTarget) this.close(); } @HostListener('document:keydown.escape') onEscape(): void { - if (this.assistantDrawer.isOpen()) { - this.close(); + if (this.assistantDrawer.isOpen() && !this.exiting()) this.close(); + } + + @HostListener('window:resize') + onResize(): void { + if (this.assistantDrawer.isOpen()) this.computeTrail(); + } + + private computeTrail(): void { + const mascotEl = document.querySelector('[aria-label="Stella helper assistant"] button'); + const drawerEl = this.drawerRef?.nativeElement; + if (!mascotEl || !drawerEl) { this.trailBubbles.set([]); return; } + + const mR = mascotEl.getBoundingClientRect(); + const dR = drawerEl.getBoundingClientRect(); + + const p0x = mR.left, p0y = mR.top + mR.height / 2; + const p2x = dR.left + 20, p2y = dR.bottom - 30; + const p1x = dR.left - 80, p1y = p2y + 100; + + const dist = Math.hypot(p2x - p0x, p2y - p0y); + const density = this.drawerExpanded() ? 35 : 45; + const count = Math.max(8, Math.min(20, Math.round(dist / density))); + + const bubbles: TrailBubble[] = []; + for (let i = 0; i < count; i++) { + const linear = i / (count - 1); + const t = Math.pow(linear, 1.8); + const mt = 1 - t; + const x = mt*mt*p0x + 2*mt*t*p1x + t*t*p2x; + const y = mt*mt*p0y + 2*mt*t*p1y + t*t*p2y; + bubbles.push({ x, y, size: 4 + 20 * linear, delay: 50 + i * 50 }); } + this.trailBubbles.set(bubbles); } } diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-assistant.service.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-assistant.service.ts new file mode 100644 index 000000000..d1dbd4805 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-assistant.service.ts @@ -0,0 +1,578 @@ +/** + * StellaAssistantService — Unified mascot + search + AI chat service. + * + * Three modes: + * tips — DB-backed, locale-aware contextual tips (default) + * search — Delegates to UnifiedSearchClient (same as Ctrl+K) + * chat — Delegates to ChatService (Advisory AI with SSE) + * + * Tips are fetched from the backend API per route + locale. + * Falls back to the static tips config if the API is unavailable. + */ + +import { Injectable, inject, signal, computed, effect } from '@angular/core'; +import { HttpClient } from '@angular/common/http'; +import { Router, NavigationEnd } from '@angular/router'; +import { filter, firstValueFrom, catchError, of, Subject, Subscription } from 'rxjs'; + +import { I18nService } from '../../../core/i18n/i18n.service'; +import { ChatService } from '../../../features/advisory-ai/chat/chat.service'; +import { SearchAssistantDrawerService } from '../../../core/services/search-assistant-drawer.service'; +import { SearchChatContextService } from '../../../core/services/search-chat-context.service'; +import { UnifiedSearchClient } from '../../../core/api/unified-search.client'; +import type { EntityCard, SynthesisResult } from '../../../core/api/unified-search.models'; +import { + StellaHelperTip, + StellaHelperPageConfig, + PAGE_TIPS, + resolvePageKey, +} from './stella-helper-tips.config'; +import type { StellaContextKey } from './stella-helper-context.service'; + +// --------------------------------------------------------------------------- +// API response models +// --------------------------------------------------------------------------- + +export interface AssistantTipDto { + tipId: string; + title: string; + body: string; + action?: { label: string; route: string }; + contextTrigger?: string; +} + +export interface AssistantTipsResponse { + greeting: string; + tips: AssistantTipDto[]; + contextTips: AssistantTipDto[]; +} + +export interface GlossaryTermDto { + termId: string; + term: string; + definition: string; + extendedHelp?: string; + relatedTerms: string[]; + relatedRoutes: string[]; +} + +export interface GlossaryResponse { + terms: GlossaryTermDto[]; +} + +export interface AssistantUserState { + seenRoutes: string[]; + completedTours: string[]; + tipPositions: Record; + dismissed: boolean; +} + +// --------------------------------------------------------------------------- +// Service +// --------------------------------------------------------------------------- + +/** Mascot modes: tips (contextual help) or search (inline results). Chat opens the full drawer. */ +export type AssistantMode = 'tips' | 'search'; + +const API_BASE = '/api/v1/stella-assistant'; +const USER_STATE_STORAGE_KEY = 'stellaops.assistant.state'; + +@Injectable({ providedIn: 'root' }) +export class StellaAssistantService { + private readonly http = inject(HttpClient); + private readonly router = inject(Router); + private readonly i18n = inject(I18nService); + private readonly assistantDrawer = inject(SearchAssistantDrawerService); + private readonly chatService = inject(ChatService); + private readonly searchClient = inject(UnifiedSearchClient); + private readonly searchChatCtx = inject(SearchChatContextService); + + private routerSub?: Subscription; + + // ---- Mode ---- + readonly mode = signal('tips'); + readonly isOpen = signal(false); + readonly isMinimized = signal(false); + + // ---- Tips (API-backed with static fallback) ---- + readonly greeting = signal(''); + readonly tips = signal([]); + readonly contextTips = signal([]); + readonly currentTipIndex = signal(0); + readonly tipsLoading = signal(false); + private tipsApiAvailable = true; // flip to false on first failure + + // ---- Context ---- + readonly currentRoute = signal('/'); + readonly currentPageKey = signal('dashboard'); + readonly activeContexts = signal([]); + + // ---- Glossary ---- + readonly glossary = signal([]); + private glossaryLoaded = false; + + // ---- Search ---- + readonly searchQuery = signal(''); + readonly searchResults = signal([]); + readonly searchSynthesis = signal(null); + readonly searchLoading = signal(false); + private searchSub?: Subscription; + + // ---- Chat animation state ---- + readonly chatAnimationState = signal<'idle' | 'thinking' | 'typing' | 'done' | 'error'>('idle'); + private chatStreamSub?: Subscription; + + // ---- Session persistence ---- + readonly activeConversationId = signal(null); + readonly conversationHistory = signal([]); + + // ---- Drawer view toggle (chat vs search) ---- + readonly drawerView = signal<'chat' | 'search'>('chat'); + + // ---- User state ---- + readonly userState = signal(this.loadLocalState()); + + // ---- Derived ---- + readonly effectiveTips = computed(() => { + const ctx = this.contextTips(); + const page = this.tips(); + // Context tips prepended (higher priority), deduped + const combined = [...ctx]; + for (const t of page) { + if (!combined.some(c => c.title === t.title)) { + combined.push(t); + } + } + return combined; + }); + + readonly totalTips = computed(() => this.effectiveTips().length); + + readonly currentTip = computed(() => { + const tips = this.effectiveTips(); + const idx = this.currentTipIndex(); + return tips[idx] ?? null; + }); + + readonly isFirstVisit = computed(() => { + const key = this.currentPageKey(); + return !this.userState().seenRoutes.includes(key); + }); + + // Chat is handled by SearchAssistantDrawerService (full ChatComponent) + + constructor() { + // Persist user state on change + effect(() => this.saveLocalState(this.userState())); + } + + // ---- Lifecycle ---- + + init(): void { + // Set initial page + this.onRouteChange(this.router.url); + + // Listen for route changes + this.routerSub = this.router.events + .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) + .subscribe(e => this.onRouteChange(e.urlAfterRedirects ?? e.url)); + + // Load glossary once + this.loadGlossary(); + } + + destroy(): void { + this.routerSub?.unsubscribe(); + } + + // ---- Mode transitions ---- + + enterTips(): void { + this.mode.set('tips'); + this.searchQuery.set(''); + } + + /** + * Activate the global search bar with a query. + * The mascot does NOT have its own search — it delegates to the + * top-bar GlobalSearchComponent for a single, consistent experience. + * + * Uses the native input setter + input event to trigger Angular's + * ngModel binding correctly (dispatchEvent with InputEvent, not Event). + */ + private searchDebounceTimer?: ReturnType; + + /** + * Search-as-you-type with 200ms debounce. + * Called on every keystroke in the mascot input. + * Results appear inline in the mascot bubble above the input. + */ + searchAsYouType(query: string): void { + this.searchQuery.set(query); + + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer); + + if (!query.trim() || query.trim().length < 2) { + this.searchResults.set([]); + this.searchSynthesis.set(null); + this.searchLoading.set(false); + return; + } + + this.searchLoading.set(true); + + this.searchDebounceTimer = setTimeout(() => { + this.searchSub?.unsubscribe(); + this.searchSub = this.searchClient.search(query.trim(), undefined, 5, { + currentRoute: this.currentRoute(), + }).subscribe({ + next: (resp) => { + this.searchResults.set(resp.cards); + this.searchSynthesis.set(resp.synthesis); + this.searchLoading.set(false); + }, + error: () => { + this.searchResults.set([]); + this.searchLoading.set(false); + }, + }); + }, 200); + } + + /** Clear search results (when input is emptied). */ + clearSearch(): void { + this.searchQuery.set(''); + this.searchResults.set([]); + this.searchSynthesis.set(null); + this.searchLoading.set(false); + this.searchSub?.unsubscribe(); + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer); + } + + /** + * Open the AI chat drawer with the full ChatComponent. + * The mascot does NOT embed chat — it delegates to the proper drawer. + * + * ALL AI chat entry points converge here: + * - Mascot "Ask Stella" button + * - Mascot question input (detected as question) + * - Search bar "Open deeper help" icon (handled by GlobalSearchComponent directly) + */ + openChat(initialMessage?: string): void { + // Pass search context to the chat if we have results + if (initialMessage || this.searchResults().length > 0) { + this.searchChatCtx.setSearchToChat({ + query: initialMessage ?? this.searchQuery(), + entityCards: this.searchResults(), + synthesis: this.searchSynthesis(), + suggestedPrompt: initialMessage ?? undefined, + }); + } + + // Close the mascot bubble — the drawer takes over + this.isOpen.set(false); + this.drawerView.set('chat'); + + // If resuming an existing conversation, don't send initialMessage again + const resuming = this.activeConversationId() && !initialMessage; + + // Open the full chat drawer + this.assistantDrawer.open({ + initialUserMessage: resuming ? null : (initialMessage ?? null), + source: 'stella_assistant', + }); + + // Connect mascot animation to chat stream events + this.chatStreamSub?.unsubscribe(); + this.chatAnimationState.set('idle'); + this.chatStreamSub = this.chatService.streamEvents.subscribe((event) => { + const type = (event as any).event ?? ''; + switch (type) { + case 'start': + case 'progress': + this.chatAnimationState.set('thinking'); + break; + case 'token': + this.chatAnimationState.set('typing'); + break; + case 'done': + this.chatAnimationState.set('done'); + setTimeout(() => this.chatAnimationState.set('idle'), 2000); + break; + case 'error': + this.chatAnimationState.set('error'); + setTimeout(() => this.chatAnimationState.set('idle'), 3000); + break; + } + }); + } + + open(): void { + this.isOpen.set(true); + this.isMinimized.set(false); + } + + // ---- Session management ---- + + setActiveConversation(id: string): void { + this.activeConversationId.set(id); + } + + newSession(): void { + const currentId = this.activeConversationId(); + if (currentId) { + this.conversationHistory.update(h => + h.includes(currentId) ? h : [...h, currentId] + ); + } + this.chatService.clearConversation(); + this.activeConversationId.set(null); + } + + toggleDrawerView(): void { + this.drawerView.update(v => v === 'chat' ? 'search' : 'chat'); + } + + close(): void { + this.isOpen.set(false); + } + + minimize(): void { + this.isMinimized.set(true); + } + + dismiss(): void { + this.isOpen.set(false); + this.userState.update(s => ({ ...s, dismissed: true })); + } + + restore(): void { + this.userState.update(s => ({ ...s, dismissed: false })); + this.isOpen.set(true); + this.isMinimized.set(false); + } + + // ---- Tips ---- + + nextTip(): void { + const max = this.totalTips() - 1; + if (this.currentTipIndex() < max) { + this.currentTipIndex.update(i => i + 1); + this.saveTipPosition(); + } + } + + prevTip(): void { + if (this.currentTipIndex() > 0) { + this.currentTipIndex.update(i => i - 1); + this.saveTipPosition(); + } + } + + navigateToAction(route: string): void { + this.close(); + this.router.navigateByUrl(route); + } + + // ---- Context ---- + + pushContext(key: StellaContextKey): void { + this.activeContexts.update(list => { + if (list.includes(key)) return list; + return [...list, key]; + }); + // Re-load tips with new context + this.loadTipsForCurrentRoute(); + } + + pushContexts(keys: StellaContextKey[]): void { + this.activeContexts.update(list => { + const next = [...list]; + let changed = false; + for (const k of keys) { + if (!next.includes(k)) { + next.push(k); + changed = true; + } + } + return changed ? next : list; + }); + this.loadTipsForCurrentRoute(); + } + + removeContext(key: StellaContextKey): void { + this.activeContexts.update(list => list.filter(k => k !== key)); + } + + clearContexts(): void { + this.activeContexts.set([]); + } + + // ---- Internal ---- + + private async onRouteChange(url: string): Promise { + const path = url.split('?')[0].split('#')[0]; + const key = resolvePageKey(url); + + if (key === this.currentPageKey() && path === this.currentRoute()) return; + + this.clearContexts(); + this.currentRoute.set(path); + this.currentPageKey.set(key); + + // Restore tip position + const saved = this.userState().tipPositions[key]; + this.currentTipIndex.set(saved ?? 0); + + // Load tips from API (or fallback to static) + await this.loadTipsForCurrentRoute(); + + // Auto-open on first visit + if (this.isFirstVisit() && !this.userState().dismissed) { + setTimeout(() => { + this.isOpen.set(true); + this.isMinimized.set(false); + this.markRouteSeen(key); + }, 800); + } + + // If in search/chat mode, return to tips on navigation + if (this.mode() === 'search') { + this.enterTips(); + } + } + + private async loadTipsForCurrentRoute(): Promise { + const route = this.currentRoute(); + const locale = this.i18n.locale(); + const contexts = this.activeContexts().join(','); + + if (this.tipsApiAvailable) { + this.tipsLoading.set(true); + try { + const params: Record = { route, locale }; + if (contexts) params['contexts'] = contexts; + + const resp = await firstValueFrom( + this.http.get(`${API_BASE}/tips`, { params }).pipe( + catchError(() => { + this.tipsApiAvailable = false; + return of(null); + }) + ) + ); + + if (resp) { + this.greeting.set(resp.greeting); + this.tips.set(resp.tips.map(this.dtoToTip)); + this.contextTips.set(resp.contextTips.map(this.dtoToTip)); + this.tipsLoading.set(false); + return; + } + } catch { + this.tipsApiAvailable = false; + } + this.tipsLoading.set(false); + } + + // Fallback to static tips + this.loadStaticTips(); + } + + private loadStaticTips(): void { + const key = this.currentPageKey(); + const config = PAGE_TIPS[key] ?? PAGE_TIPS['default']; + this.greeting.set(config.greeting); + this.tips.set(config.tips); + + // Find context-triggered tips from static config + const contexts = this.activeContexts(); + if (contexts.length > 0) { + const ctxTips: StellaHelperTip[] = []; + for (const cfg of Object.values(PAGE_TIPS)) { + for (const tip of cfg.tips) { + if (tip.contextTrigger && contexts.includes(tip.contextTrigger)) { + if (!ctxTips.some(t => t.title === tip.title)) { + ctxTips.push(tip); + } + } + } + } + this.contextTips.set(ctxTips); + } else { + this.contextTips.set([]); + } + } + + private async loadGlossary(): Promise { + if (this.glossaryLoaded) return; + try { + const locale = this.i18n.locale(); + const resp = await firstValueFrom( + this.http.get(`${API_BASE}/glossary`, { params: { locale } }).pipe( + catchError(() => of({ terms: [] })) + ) + ); + this.glossary.set(resp.terms); + this.glossaryLoaded = true; + } catch { + // Glossary unavailable, not critical + } + } + + private markRouteSeen(key: string): void { + this.userState.update(s => { + if (s.seenRoutes.includes(key)) return s; + return { ...s, seenRoutes: [...s.seenRoutes, key] }; + }); + } + + private saveTipPosition(): void { + const key = this.currentPageKey(); + const idx = this.currentTipIndex(); + this.userState.update(s => ({ + ...s, + tipPositions: { ...s.tipPositions, [key]: idx }, + })); + } + + private dtoToTip(dto: AssistantTipDto): StellaHelperTip { + return { + title: dto.title, + body: dto.body, + action: dto.action, + contextTrigger: dto.contextTrigger, + }; + } + + // ---- Persistence (localStorage with future API sync) ---- + + private loadLocalState(): AssistantUserState { + try { + const raw = localStorage.getItem(USER_STATE_STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + return { + seenRoutes: Array.isArray(parsed.seenRoutes) ? parsed.seenRoutes : [], + completedTours: Array.isArray(parsed.completedTours) ? parsed.completedTours : [], + tipPositions: parsed.tipPositions && typeof parsed.tipPositions === 'object' ? parsed.tipPositions : {}, + dismissed: typeof parsed.dismissed === 'boolean' ? parsed.dismissed : false, + }; + } + } catch { /* ignore */ } + return { seenRoutes: [], completedTours: [], tipPositions: {}, dismissed: false }; + } + + private saveLocalState(state: AssistantUserState): void { + try { + localStorage.setItem(USER_STATE_STORAGE_KEY, JSON.stringify(state)); + } catch { /* ignore */ } + } + + // Future: sync state to backend API + // async syncStateToServer(): Promise { + // await firstValueFrom( + // this.http.put(`${API_BASE}/user-state`, this.userState()) + // ); + // } +} diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper-context.service.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper-context.service.ts new file mode 100644 index 000000000..7566ddd35 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper-context.service.ts @@ -0,0 +1,153 @@ +import { Injectable, signal, computed } from '@angular/core'; + +/** + * StellaHelperContextService — Reactive context injection for the Stella Helper. + * + * Page/tab components inject this service and push context signals when they + * detect states that the helper should react to. The helper subscribes to + * these signals and surfaces context-triggered tips with priority. + * + * Architecture: + * - Components push context keys (e.g., 'sbom-missing', 'gate-blocked') + * - Helper reads activeContexts() signal and matches against tip.contextTrigger + * - Context-triggered tips take priority over generic page tips + * - Contexts are cleared automatically on navigation (page change) + * + * Usage in a component: + * ```ts + * private helperCtx = inject(StellaHelperContextService); + * + * ngOnInit() { + * if (this.environments().some(e => e.sbomStatus === 'missing')) { + * this.helperCtx.push('sbom-missing'); + * } + * if (this.riskBudget() > 70) { + * this.helperCtx.push('budget-exceeded'); + * } + * } + * ``` + */ + +/** Well-known context keys that page components can push. */ +export type StellaContextKey = + // Dashboard / global + | 'sbom-missing' + | 'health-unknown' + | 'no-environments' + | 'feed-stale' + | 'feed-never-synced' + // Releases + | 'gate-blocked' + | 'gate-warn' + | 'evidence-missing' + | 'release-draft' + | 'release-failed' + | 'approval-pending' + | 'approval-expired' + // Security + | 'critical-open' + | 'high-open' + | 'no-findings' + | 'no-sbom-components' + | 'reachability-low' + | 'unknowns-present' + // Policy + | 'budget-exceeded' + | 'budget-critical' + | 'no-baseline' + | 'policy-conflict' + | 'sealed-mode-active' + | 'shadow-mode-active' + // VEX + | 'vex-zero' + | 'vex-conflicts' + | 'exceptions-pending' + | 'exceptions-expiring' + // Evidence + | 'no-capsules' + | 'no-audit-events' + | 'proof-chain-broken' + // Operations + | 'dead-letters' + | 'jobs-failed' + | 'agents-none' + | 'agents-degraded' + | 'signals-degraded' + // Integrations + | 'no-integrations' + | 'integration-error' + // Empty states (generic) + | 'empty-table' + | 'empty-list' + | 'first-visit' + // Custom (string allows extension without modifying the type) + | (string & {}); + +@Injectable({ providedIn: 'root' }) +export class StellaHelperContextService { + /** Active context keys pushed by page components. */ + private readonly _contexts = signal>(new Set()); + + /** Read-only signal of active contexts. */ + readonly activeContexts = computed(() => [...this._contexts()]); + + /** Whether any context is currently active. */ + readonly hasContext = computed(() => this._contexts().size > 0); + + /** + * Push a context key. The helper will check if any tips match + * this context and surface them with priority. + */ + push(key: StellaContextKey): void { + this._contexts.update(set => { + if (set.has(key)) return set; + const next = new Set(set); + next.add(key); + return next; + }); + } + + /** + * Push multiple context keys at once. + */ + pushAll(keys: StellaContextKey[]): void { + this._contexts.update(set => { + const next = new Set(set); + let changed = false; + for (const k of keys) { + if (!next.has(k)) { + next.add(k); + changed = true; + } + } + return changed ? next : set; + }); + } + + /** + * Remove a specific context (e.g., when state changes). + */ + remove(key: StellaContextKey): void { + this._contexts.update(set => { + if (!set.has(key)) return set; + const next = new Set(set); + next.delete(key); + return next; + }); + } + + /** + * Clear all contexts. Called automatically on route change + * by the StellaHelperComponent. + */ + clear(): void { + this._contexts.set(new Set()); + } + + /** + * Check if a specific context is active. + */ + has(key: StellaContextKey): boolean { + return this._contexts().has(key); + } +} diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper-tips.config.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper-tips.config.ts new file mode 100644 index 000000000..a849465ab --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper-tips.config.ts @@ -0,0 +1,1355 @@ +/** + * Stella Helper — Page-Contextual Tips Configuration + * + * Each route key maps to an array of tips for that page. + * Tips are shown in order; the helper cycles through them. + * Each tip has a short title, body, and optional action link. + * + * Written for "Alex" — a mid-level developer who just became DevOps + * and has zero knowledge of SBOM, VEX, reachability, policy gates, etc. + */ + +export interface StellaHelperTip { + /** Short title (shown bold in bubble) */ + title: string; + /** Body text — plain English, no jargon without explanation */ + body: string; + /** Optional CTA button */ + action?: { label: string; route: string }; + /** Optional "learn more" link */ + learnMore?: string; + /** + * Optional context trigger — when a page component pushes this context key + * via StellaHelperContextService, this tip is shown with priority. + * Examples: 'sbom-missing', 'gate-blocked', 'feed-stale', 'budget-exceeded' + */ + contextTrigger?: string; +} + +export interface StellaHelperPageConfig { + /** Greeting shown when helper first appears on this page */ + greeting: string; + /** Tips to cycle through */ + tips: StellaHelperTip[]; +} + +/** + * Match a URL path to a page config key. + * Order matters — first match wins. Use startsWith for prefix matching. + */ +export function resolvePageKey(url: string): string { + // Strip query params and fragments + const path = url.split('?')[0].split('#')[0]; + + // Exact matches first + const exactMap: Record = { + '/': 'dashboard', + '/welcome': 'welcome', + }; + if (exactMap[path]) return exactMap[path]; + + // Prefix matches (order = most specific first) + const prefixes: [string, string][] = [ + // ── Release Control (tab-level) ── + ['/releases/deployments/new', 'deployments-create'], + ['/releases/deployments', 'deployments'], + ['/environments/overview', 'readiness'], + ['/releases/promotions/create', 'promotions-create'], + ['/releases/promotions', 'promotions'], + ['/releases/approvals/', 'approval-detail'], + ['/releases/approvals', 'approvals'], + ['/releases/investigation/timeline', 'investigation-timeline'], + ['/releases/investigation/deploy-diff', 'deploy-diff'], + ['/releases/investigation/change-trace', 'change-trace'], + ['/releases/hotfixes', 'hotfixes'], + ['/releases/bundles', 'release-bundles'], + ['/releases/versions/new', 'version-create'], + ['/releases/versions/', 'version-detail'], + ['/releases/versions', 'releases-versions'], + ['/releases/promotion-graph', 'promotion-graph'], + ['/releases/workflows', 'release-workflows'], + ['/releases/new', 'releases'], + ['/releases/detail/', 'release-detail'], + ['/releases', 'releases'], + ['/environments/targets', 'env-targets'], + ['/environments/hosts/', 'env-host-detail'], + ['/environments/hosts', 'env-hosts'], + ['/environments/', 'env-detail'], + ['/environments', 'environments'], + + // ── Security (tab-level) ── + ['/triage/artifacts/', 'triage-workspace'], + ['/triage', 'vulnerabilities'], + ['/security/supply-chain-data', 'supply-chain'], + ['/security/findings/', 'finding-detail'], + ['/security/findings', 'findings'], + ['/security/reachability', 'reachability'], + ['/security/unknowns', 'unknowns'], + ['/security/scan', 'scan-image'], + ['/security/sbom/', 'sbom-explorer'], + ['/security/advisories-vex', 'advisories-vex'], + ['/security/disposition', 'disposition'], + ['/security/connectivity', 'connectivity'], + ['/security/reports', 'security-reports'], + ['/security/triage/', 'security-triage-detail'], + ['/security/triage', 'security-triage'], + ['/security', 'security-posture'], + + // ── VEX & Exceptions (tab-level — 8 tabs) ── + ['/ops/policy/vex/search/detail/', 'vex-statement-detail'], + ['/ops/policy/vex/search', 'vex-search'], + ['/ops/policy/vex/create', 'vex-create'], + ['/ops/policy/vex/stats', 'vex-stats'], + ['/ops/policy/vex/consensus', 'vex-consensus'], + ['/ops/policy/vex/explorer', 'vex-explorer'], + ['/ops/policy/vex/conflicts', 'vex-conflicts'], + ['/ops/policy/vex/exceptions/approvals', 'exception-approvals'], + ['/ops/policy/vex/exceptions/', 'exception-detail'], + ['/ops/policy/vex/exceptions', 'vex-exceptions'], + ['/ops/policy/vex', 'vex'], + + // ── Policy Governance (tab-level — 5+ tabs) ── + ['/ops/policy/governance/budget/config', 'governance-budget-config'], + ['/ops/policy/governance/budget', 'governance-budget'], + ['/ops/policy/governance/profiles/new', 'governance-profile-new'], + ['/ops/policy/governance/profiles/', 'governance-profile-edit'], + ['/ops/policy/governance/profiles', 'governance-profiles'], + ['/ops/policy/governance/conflicts/', 'governance-conflict-resolve'], + ['/ops/policy/governance/conflicts', 'governance-conflicts'], + ['/ops/policy/governance/trust-weights', 'governance-trust-weights'], + ['/ops/policy/governance/staleness', 'governance-staleness'], + ['/ops/policy/governance/sealed-mode/overrides', 'governance-sealed-overrides'], + ['/ops/policy/governance/sealed-mode', 'governance-sealed-mode'], + ['/ops/policy/governance/validator', 'governance-validator'], + ['/ops/policy/governance/impact-preview', 'governance-impact'], + ['/ops/policy/governance/schema-playground', 'governance-schema-playground'], + ['/ops/policy/governance/schema-docs', 'governance-schema-docs'], + ['/ops/policy/governance', 'governance'], + + // ── Policy Simulation (tab-level — 6 tabs) ── + ['/ops/policy/simulation/shadow', 'sim-shadow'], + ['/ops/policy/simulation/promotion-gate', 'sim-promotion-gate'], + ['/ops/policy/simulation/test-validate', 'sim-test-validate'], + ['/ops/policy/simulation/pre-promotion', 'sim-pre-promotion'], + ['/ops/policy/simulation/effective', 'sim-effective'], + ['/ops/policy/simulation/exceptions', 'sim-exceptions'], + ['/ops/policy/simulation', 'simulation'], + + // ── Policy Audit (tab-level) ── + ['/ops/policy/audit/vex', 'policy-audit-vex'], + ['/ops/policy/audit/log/events', 'policy-audit-events'], + ['/ops/policy/audit/log', 'policy-audit-log'], + ['/ops/policy/audit', 'policy-audit'], + + // ── Policy Packs (sub-routes) ── + ['/ops/policy/packs/', 'policy-pack-detail'], + ['/ops/policy/packs', 'policy-packs'], + + // ── Policy Gates ── + ['/ops/policy/gates/catalog', 'gate-catalog'], + ['/ops/policy/gates/simulate/', 'gate-simulate'], + + // ── Evidence (tab-level) ── + ['/evidence/overview', 'evidence-overview'], + ['/evidence/threads/', 'evidence-thread-detail'], + ['/evidence/threads', 'evidence-threads'], + ['/evidence/capsules/', 'capsule-detail'], + ['/evidence/capsules', 'decision-capsules'], + ['/evidence/verify-replay', 'evidence-replay'], + ['/evidence/proofs', 'evidence-proofs'], + ['/evidence/proof-chain', 'evidence-proofs'], + ['/evidence/bundles/new', 'evidence-bundle-create'], + ['/evidence/bundles', 'evidence-bundles'], + ['/evidence/audit-log/events', 'audit-log-events'], + ['/evidence/audit-log/export', 'audit-log-export'], + ['/evidence/audit-log', 'audit-log'], + ['/evidence/exports', 'export-center'], + ['/evidence/workspaces/auditor/', 'workspace-auditor'], + ['/evidence/workspaces/developer/', 'workspace-developer'], + ['/evidence', 'evidence-overview'], + + // ── Operations (tab-level) ── + ['/ops/operations/jobengine/jobs/', 'job-detail'], + ['/ops/operations/jobengine/quotas', 'job-quotas'], + ['/ops/operations/jobengine', 'scheduled-jobs'], + ['/ops/operations/feeds-airgap', 'feeds-airgap'], + ['/ops/operations/agents/', 'agent-detail'], + ['/ops/operations/agents', 'agent-fleet'], + ['/ops/operations/signals', 'signals'], + ['/ops/operations/doctor', 'diagnostics'], + ['/ops/operations/health-slo', 'health-slo'], + ['/ops/operations/scheduler', 'scheduler'], + ['/ops/operations/offline-kit', 'offline-kit'], + ['/ops/operations/dead-letter', 'dead-letter'], + ['/ops/operations/aoc', 'aoc-compliance'], + ['/ops/operations/ai-runs/', 'ai-run-detail'], + ['/ops/operations/ai-runs', 'ai-runs'], + ['/ops/operations/notifications', 'notifications'], + ['/ops/operations/watchlist', 'watchlist'], + ['/ops/operations/trust-analytics', 'trust-analytics'], + ['/ops/operations/drift', 'runtime-drift'], + ['/ops/operations/event-stream', 'event-stream'], + ['/ops/operations/status', 'system-status'], + ['/ops/operations', 'operations-hub'], + ['/ops/scripts', 'scripts'], + ['/ops/scanner-ops', 'scanner-ops'], + + // ── Settings (tab-level) ── + ['/setup/integrations', 'integrations'], + ['/setup/identity-access', 'identity-access'], + ['/setup/identity-providers', 'identity-providers'], + ['/setup/trust-signing/issuers', 'trust-issuers'], + ['/setup/trust-signing', 'certificates-trust'], + ['/setup/tenant-branding', 'theme-branding'], + ['/setup/preferences', 'user-preferences'], + ['/setup/notifications', 'notification-settings'], + ['/setup/usage', 'usage-limits'], + ['/setup/system', 'system-settings'], + ['/setup/offline', 'offline-settings'], + ['/setup/ai-preferences', 'ai-preferences'], + ['/setup/workflows', 'workflow-settings'], + + // ── Console Admin ── + ['/console/admin/tenants', 'admin-tenants'], + ['/console/admin/users', 'admin-users'], + ['/console/admin/roles', 'admin-roles'], + ['/console/admin/clients', 'admin-clients'], + ['/console/admin/tokens', 'admin-tokens'], + + // ── Mission Control ── + ['/mission-control/alerts', 'mission-alerts'], + ['/mission-control/activity', 'mission-activity'], + ['/mission-control', 'dashboard'], + ]; + + for (const [prefix, key] of prefixes) { + if (path.startsWith(prefix)) return key; + } + return 'default'; +} + +// --------------------------------------------------------------------------- +// TIP CONTENT — organized per page +// --------------------------------------------------------------------------- + +export const PAGE_TIPS: Record = { + // ========================================================================= + // DASHBOARD + // ========================================================================= + dashboard: { + greeting: "Welcome to your command center! I'm Stella, your DevOps guide. Let me show you around.", + tips: [ + { + title: 'What is this dashboard?', + body: 'This is your daily operations overview. It shows the health of every environment, open vulnerabilities, SBOM coverage, and feed status — all in real-time. Start each day here.', + }, + { + title: 'What does "SBOM: missing" mean?', + body: 'SBOM stands for Software Bill of Materials — it\'s a list of every package, library, and binary inside a container image. "Missing" means no images have been scanned yet. Scan one to populate this!', + action: { label: 'Scan your first image', route: '/security/scan' }, + }, + { + title: 'Understanding severity levels', + body: 'Critical = remotely exploitable, fix immediately. High = significant risk, fix within days. Medium = moderate, next sprint. Low = minimal, track and fix when convenient.', + }, + { + title: 'What are "Feeds"?', + body: 'Feeds are vulnerability databases (NVD, OSV) that Stella syncs automatically. When a new CVE is published, Stella checks if any of your scanned images are affected. Keep feeds fresh!', + action: { label: 'Check feed status', route: '/ops/operations/feeds-airgap' }, + }, + { + title: 'Recommended first steps', + body: '1) Run Diagnostics to verify services are healthy. 2) Connect a container registry. 3) Scan your first image. 4) Review findings. 5) Create your first release.', + action: { label: 'Run Diagnostics', route: '/ops/operations/doctor' }, + }, + { + title: 'The status bar at the top', + body: 'The colored dots at the top right show platform health: Events (real-time stream), Policy (active rules), Evidence (signing), Feed (advisory sync), and Offline (connectivity). Green = good.', + }, + { + title: 'Ctrl+K — your power shortcut', + body: 'Press Ctrl+K to open the command palette. From there you can search anything, jump to any page, run scans, and access quick actions without using the sidebar.', + }, + ], + }, + + // ========================================================================= + // RELEASE CONTROL + // ========================================================================= + deployments: { + greeting: "This is where deployments happen! Let me explain how releases move between environments.", + tips: [ + { + title: 'What is a deployment?', + body: 'A deployment is when a release moves from one environment to another (e.g., Staging to Production). Each deployment goes through configured gates — security scans, policy checks, and human approvals — before it can proceed.', + }, + { + title: 'What are "Gates"?', + body: 'Gates are checkpoints a release must pass before promotion. Think of them like airport security: your release (the passenger) must clear each gate (metal detector, passport check, boarding pass) before boarding (deploying).', + }, + { + title: 'Pending Approvals', + body: 'When a release passes all automated gates, it may still need human approval. You\'ll see pending approvals here. Review the evidence, check the gate status, then Approve or Reject.', + }, + { + title: 'Approve vs Reject — when to use each', + body: 'Approve when: all gates pass, evidence is verified, and you\'re confident in the release. Reject when: something looks wrong, gates show warnings you don\'t accept, or you need more info.', + }, + ], + }, + + releases: { + greeting: "Releases are the heart of Stella Ops. Each one is a verified, immutable bundle of your container images.", + tips: [ + { + title: 'What is a Release?', + body: 'A release bundles one or more container images by their immutable SHA256 digest — not by tag. This ensures you deploy exactly what was scanned. No one can swap the image behind your back.', + }, + { + title: 'Understanding the columns', + body: 'Gates: PASS/WARN/BLOCK status of security checks. Risk: aggregate vulnerability severity. Evidence: whether cryptographic proof exists. Status: Draft → Ready → Deployed (or Failed).', + }, + { + title: 'Why "Evidence: Missing" matters', + body: 'Missing evidence means no Decision Capsule exists for this release. Without evidence, you can\'t prove your release decision to auditors. Scans and policy evaluations generate evidence automatically.', + }, + { + title: 'The Promote action', + body: 'Click "Promote" to move a release to the next environment. Stella will evaluate all gates, collect approvals, and record the decision as signed evidence. The entire chain is auditable.', + action: { label: 'Create a new release', route: '/releases/new' }, + }, + ], + }, + + environments: { + greeting: "This topology map shows how releases flow between your environments.", + tips: [ + { + title: 'Reading the topology map', + body: 'Each box is an environment — an isolated deployment target with its own security policies and approval rules. Arrows show promotion paths: releases flow from Dev → Staging → Production.', + }, + { + title: 'Why you can\'t skip environments', + body: 'Releases must pass through each environment in order. This ensures every release is tested in staging before production. The gates at each environment may have different requirements.', + }, + { + title: 'Environment health colors', + body: 'Green = healthy, everything running fine. Yellow = degraded, some issues detected. Red = blocked, releases can\'t be promoted here. Grey/Unknown = no agent reporting from this environment.', + }, + ], + }, + + readiness: { + greeting: "Readiness shows whether your environments are prepared to accept new releases.", + tips: [ + { + title: 'What is Readiness?', + body: 'Readiness checks if deployment targets are online, have enough resources, and meet policy requirements. Think of it like pre-flight checks: is the runway clear, is there fuel, is the crew ready?', + }, + { + title: 'Empty readiness?', + body: 'If you see no data, it means no deployment agents are connected or no targets are configured yet. Deploy an agent to your first host to start seeing readiness data.', + action: { label: 'Deploy an Agent', route: '/ops/operations/agents' }, + }, + ], + }, + + // ========================================================================= + // SECURITY + // ========================================================================= + vulnerabilities: { + greeting: "Time to triage! This is where you review and decide what to do about each vulnerability.", + tips: [ + { + title: 'What does "triage" mean?', + body: 'Triage means deciding what to do about each vulnerability finding: Fix it (create a task), Accept the risk (with documented justification), or mark it Not Applicable (with proof). Every decision becomes auditable evidence.', + }, + { + title: 'The triage workflow', + body: '1) Select an artifact (scanned container image). 2) Review findings by severity — criticals first. 3) For each: fix, accept risk, or mark not-applicable. 4) When all criticals/highs are addressed, the release becomes "Ready".', + }, + { + title: 'What is an "artifact"?', + body: 'An artifact is a scanned container image, identified by its SHA256 digest. Each artifact has an SBOM (what\'s inside it) and findings (what\'s wrong with it).', + }, + { + title: 'Opening a Workspace', + body: 'Click "Open Workspace" to get a full investigation view for an artifact — SBOM, findings, reachability evidence, and VEX statements all in one place.', + }, + ], + }, + + 'security-posture': { + greeting: "Security Posture gives you the big picture — how secure is your entire estate right now?", + tips: [ + { + title: 'Risk Posture score', + body: 'Your risk posture is calculated from unresolved vulnerabilities weighted by severity and reachability. Target MEDIUM or lower for production environments. HIGH means you have critical findings needing attention.', + }, + { + title: 'VEX Coverage', + body: 'VEX Coverage shows how many findings have formal exploitability statements. Higher coverage = fewer "unknown" findings = better signal-to-noise ratio in your security data.', + }, + { + title: '"Start a scan" tip', + body: 'If SBOM health is empty, you need to scan container images first. That populates everything: vulnerability data, reachability analysis, and supply-chain coverage.', + action: { label: 'Scan an image', route: '/security/scan' }, + }, + ], + }, + + 'supply-chain': { + greeting: "Supply-chain data is the inventory of everything inside your containers.", + tips: [ + { + title: 'What is Supply-Chain Data?', + body: 'It\'s the complete list of OS packages, language libraries, native binaries, and dependency trees inside your container images. This powers vulnerability matching, license compliance, and drift detection.', + }, + { + title: 'Why is it empty?', + body: 'Supply-chain data is generated when you scan a container image. The scanner extracts every component and maps dependencies. Scan your first image to populate this view.', + action: { label: 'Scan an image', route: '/security/scan' }, + }, + { + title: 'SBOM formats', + body: 'Stella generates SBOMs in industry-standard formats: SPDX 3.0 and CycloneDX 1.7. These can be exported and shared with customers, auditors, or regulatory bodies.', + }, + ], + }, + + findings: { + greeting: "The Findings Explorer lets you dive deep into specific vulnerabilities and compare scans.", + tips: [ + { + title: 'What is a "baseline"?', + body: 'A baseline is a known-good reference scan. When you select one, Stella shows what CHANGED — new vulnerabilities added, old ones fixed. This "Smart-Diff" separates real regressions from inherited noise.', + }, + { + title: 'Comparison evidence', + body: 'When you compare scans against a baseline, Stella generates comparison evidence showing exactly what risk changed. This is powerful for release decisions: "Is this release MORE or LESS risky than what\'s already deployed?"', + }, + ], + }, + + reachability: { + greeting: "Reachability answers the most important security question: can an attacker actually reach this vulnerability?", + tips: [ + { + title: 'Why reachability matters', + body: 'A critical CVE in a library you imported but never call is noise. Reachability analysis traces whether vulnerable code is actually callable from your application\'s entry points. This can turn 200 "criticals" into 12 real ones.', + }, + { + title: 'Hybrid analysis approach', + body: 'Stella uses three layers: Static analysis (traces call graphs in your code), Runtime signals (observes what actually executes in practice), and Confidence scoring (from "theoretical" to "confirmed exploitable").', + }, + { + title: 'Coverage percentage', + body: 'Coverage shows what percentage of your codebase has been analyzed. Higher = more confident decisions. Target >80% for production. Low coverage means more unknowns in your risk assessment.', + }, + ], + }, + + unknowns: { + greeting: "Unknowns are blind spots — components the scanner couldn't fully identify.", + tips: [ + { + title: 'What are Unknowns?', + body: 'Unknowns are stripped binaries without debug symbols, obfuscated code, or packages from sources not in any advisory database. You can\'t assess risk for something you can\'t identify.', + }, + { + title: 'How to resolve them', + body: 'Options: Upload debug symbols for binary analysis, add manual annotations about what you know, use Advisory AI for identification, or accept the risk with documented justification.', + }, + { + title: 'Zero unknowns is good!', + body: 'If you see all zeros — great! Your entire supply chain is identified. All components can be matched against vulnerability databases for accurate risk assessment.', + }, + ], + }, + + 'scan-image': { + greeting: "Ready to scan? Submit a container image reference to get a full security analysis.", + tips: [ + { + title: 'What happens when you scan', + body: '1) Image layers are pulled and analyzed. 2) SBOM is generated (all packages, libs, binaries). 3) Vulnerabilities matched against feeds. 4) Reachability analysis checks exploitability. 5) Results appear in Security Posture within ~2-5 minutes.', + }, + { + title: 'Digest vs Tag', + body: 'Use a digest (sha256:abc...) instead of a tag (:latest) when possible. Tags can be overwritten — someone could push a different image to the same tag. Digests are immutable and guarantee you scan exactly what you intend.', + }, + { + title: 'Format examples', + body: 'registry.example.com/myapp:v2.1.0 or ghcr.io/org/service@sha256:abc123... or docker.io/library/nginx:1.25 — any standard OCI image reference works.', + }, + ], + }, + + // ========================================================================= + // VEX & POLICY + // ========================================================================= + vex: { + greeting: "VEX is one of the most powerful features in Stella. Let me demystify it for you.", + tips: [ + { + title: 'What is VEX?', + body: 'VEX (Vulnerability Exploitability eXchange) is a formal way to say: "Yes, our software uses this library, but this specific vulnerability doesn\'t affect us because [reason]." It\'s like a doctor\'s note for vulnerabilities.', + }, + { + title: 'VEX statuses explained', + body: 'Affected = vulnerability impacts your software, action needed. Not Affected = vulnerable code path is unreachable in your config. Fixed = patched. Under Investigation = still determining impact.', + }, + { + title: 'Why VEX reduces noise by 60-80%', + body: 'Without VEX, every scan produces hundreds of theoretically-true but practically-irrelevant findings. VEX lets you document WHY certain findings aren\'t exploitable, and auto-suppress them in future scans.', + }, + { + title: 'Creating your first VEX statement', + body: 'Go to Vulnerabilities → select a finding → click "Create VEX Statement" → choose status and provide justification. The statement is cryptographically signed and stored as auditable evidence.', + action: { label: 'View Vulnerabilities', route: '/triage/artifacts' }, + }, + { + title: 'VEX Consensus', + body: 'When multiple sources publish VEX statements about the same vulnerability (vendor, scanner, your team), Stella uses trust-weighted consensus to determine the effective status. Higher-trust sources carry more weight.', + }, + ], + }, + + governance: { + greeting: "Risk & Governance is where you set the security rules for your organization.", + tips: [ + { + title: 'What is a Risk Budget?', + body: 'Think of it like a credit limit for security debt. It measures what percentage of your allowed risk capacity is consumed. When it exceeds thresholds (70% = warning, 90% = critical), promotions may be blocked.', + }, + { + title: 'Budget vs. hard blocks', + body: 'The risk budget is a soft gate — it warns and can block, but you can configure overrides. Some rules (like "no known-exploited CVEs in production") can be hard blocks with no override possible.', + }, + { + title: 'Top Contributors', + body: 'The chart shows which vulnerabilities, components, or images contribute most to your risk budget. Focus remediation efforts here for the biggest risk reduction.', + }, + ], + }, + + simulation: { + greeting: "Shadow mode lets you test policy changes safely before they affect real releases.", + tips: [ + { + title: 'What is Shadow Mode?', + body: 'Shadow mode runs proposed policy rules alongside active ones and shows where they would produce different decisions — without actually blocking anything. Test before you enforce!', + }, + { + title: 'Use cases', + body: '"If I tighten the severity threshold, how many releases would be blocked?" or "Will this new compliance rule break existing deployments?" Shadow mode answers these safely.', + }, + ], + }, + + 'policy-audit': { + greeting: "Every policy action in your organization is recorded here — immutable and signed.", + tips: [ + { + title: 'What gets audited?', + body: 'Promotions (when a release moved), Approvals (who approved what), Rejections (what was blocked and why), and Simulations (shadow mode comparisons). Events are generated automatically.', + }, + { + title: 'Immutable audit trail', + body: 'Unlike regular logs, audit events in Stella are cryptographically signed. They can\'t be modified or deleted after creation. This meets compliance requirements for regulated industries.', + }, + ], + }, + + 'policy-packs': { + greeting: "Policy packs are bundles of security rules — like security profiles for your environments.", + tips: [ + { + title: 'What is a Policy Pack?', + body: 'A policy pack is a collection of rules defining what your organization allows in releases. Examples: "Production Strict" (no criticals, 2 approvers), "Dev Relaxed" (allow highs, single approver).', + }, + { + title: 'Pack lifecycle', + body: 'Create a pack → write rules in Stella DSL or YAML → simulate against existing releases → review and approve → activate. Active packs are evaluated on every promotion.', + }, + { + title: 'Setting a baseline', + body: 'One pack should be set as your baseline — the default rules that apply everywhere. Additional packs can override or extend the baseline per-environment.', + }, + ], + }, + + // ========================================================================= + // EVIDENCE + // ========================================================================= + 'evidence-overview': { + greeting: "Evidence is what makes Stella unique — every decision has tamper-proof, signed proof.", + tips: [ + { + title: 'What is Evidence?', + body: 'Evidence is the signed, cryptographic record of every scan, decision, approval, and deployment. Unlike logs that can be edited, evidence is sealed — providing audit-grade proof for regulators, customers, or your future self.', + }, + { + title: 'Evidence types', + body: 'Scan evidence (SBOM + findings), Policy evidence (rules evaluated + outcomes), Approval evidence (who + when + why), Deployment evidence (what was deployed where), and Proof Chains (linking everything together).', + }, + { + title: 'Offline verification', + body: 'Evidence bundles are self-contained. You can verify them without network access using bundled trust roots. Ship evidence to an air-gapped environment and it\'s still verifiable.', + }, + ], + }, + + 'decision-capsules': { + greeting: "Decision Capsules are the crown jewel — complete proof packages for every release decision.", + tips: [ + { + title: 'What is a Decision Capsule?', + body: 'A sealed package containing: exact SBOM at scan time, vulnerability findings, VEX statements, reachability evidence, policy rules evaluated, approval records, and cryptographic signatures over everything.', + }, + { + title: 'Why capsules matter', + body: 'If an auditor asks "why did you release this?" — hand them the capsule. It\'s self-contained, offline-verifiable, and can be deterministically replayed to prove the decision was correct at that point in time.', + }, + { + title: 'Deterministic replay', + body: 'Capsules can be "replayed" — feed the same inputs through the same policy and verify you get the same outputs. This proves the decision wasn\'t manipulated.', + }, + ], + }, + + 'audit-log': { + greeting: "The audit log captures 200+ event types across the entire platform.", + tips: [ + { + title: 'What gets logged?', + body: 'Release scans and promotions, policy changes and activations, VEX statements and consensus decisions, integration configuration changes, and identity/access management events.', + }, + { + title: 'Anomaly detection', + body: 'Check the Timeline tab for chronological event visualization. The Correlation tab helps link related events across modules. Unusual patterns (bulk approvals, off-hours changes) are highlighted.', + }, + ], + }, + + 'export-center': { + greeting: "Export evidence bundles for auditors, compliance, or air-gapped environments.", + tips: [ + { + title: 'Export profiles', + body: 'StellBundle: signed audit bundle with DSSE envelopes for external auditors. Daily Compliance: automated daily reports with SBOMs and scans. Audit Bundle: complete evidence for external review.', + }, + { + title: 'When to export', + body: 'Before external audits (SOC 2, ISO 27001), when sharing security posture with customers, for regulatory submissions, or when transferring evidence to air-gapped environments.', + }, + ], + }, + + // ========================================================================= + // OPERATIONS + // ========================================================================= + 'operations-hub': { + greeting: "Start your day here! The Operations Hub shows everything that needs your attention right now.", + tips: [ + { + title: 'Daily ops workflow', + body: 'Check blocking issues first (these prevent releases), then pending operator actions, then review budget health across categories. Items marked "Open" need your action.', + }, + { + title: 'Budget categories', + body: 'Blocking Sub: items preventing releases. Blocking: potential blockers. Events: platform events. Health: service status. Supply & Airgap: feed freshness. Capacity: resource usage.', + }, + { + title: 'Critical diagnostics', + body: 'The bottom section shows critical diagnostic results. If any services are unhealthy, they\'ll appear here. Click to open the full Diagnostics page for remediation steps.', + action: { label: 'Full Diagnostics', route: '/ops/operations/doctor' }, + }, + ], + }, + + 'scheduled-jobs': { + greeting: "Jobs run behind the scenes — scans, promotions, scheduled tasks, and recovery.", + tips: [ + { + title: 'What creates jobs?', + body: 'Jobs are created automatically when releases are promoted, scans are triggered, or scheduled tasks run. You can also create manual jobs for one-off operations.', + }, + { + title: 'Dead-Letter Recovery', + body: 'When a job fails, it goes to the dead-letter queue. You can retry, inspect the failure, or dismiss it. Don\'t let dead letters pile up — they may indicate infrastructure issues.', + }, + { + title: 'Execution quotas', + body: 'Quotas prevent runaway jobs from consuming all resources. Check token usage and concurrency limits here. If jobs are waiting, you may need to increase quota allocation.', + }, + ], + }, + + 'feeds-airgap': { + greeting: "Feeds power your vulnerability detection. No feeds = no vulnerability matching.", + tips: [ + { + title: 'What are advisory feeds?', + body: 'Vulnerability databases from NVD (US government), OSV (Google), and other sources. Stella syncs these automatically and matches them against your scanned images to find known vulnerabilities.', + }, + { + title: 'Feed freshness matters', + body: 'Stale feeds = missed vulnerabilities. If a new CVE was published yesterday and your feeds haven\'t synced, Stella won\'t flag it. Check "Last Sync" timestamps regularly.', + }, + { + title: 'Air-gap mode', + body: 'For environments without internet, Stella can package feeds, images, and tools into offline bundles. Import them on the air-gapped side. The staleness budget controls how old feeds can be before blocking promotions.', + }, + ], + }, + + 'agent-fleet': { + greeting: "Agents are the bridge between Stella and your deployment targets.", + tips: [ + { + title: 'What are agents?', + body: 'Lightweight services you install on Docker hosts, VMs, or other targets. They receive deployment instructions from Stella and execute them (docker-compose up, docker run, etc.), reporting results back with evidence.', + }, + { + title: 'How to deploy an agent', + body: 'Download the agent binary, install it on your target host, and register it with Stella using a token. The agent joins a group and starts reporting health. Stella then knows this target is available for deployments.', + }, + { + title: 'Agent groups', + body: 'Organize agents into groups by role (web-servers, databases), region (us-east, eu-west), or environment (dev, staging, prod). Groups are used in deployment targeting.', + }, + ], + }, + + signals: { + greeting: "Signals are runtime probes that collect execution data from your running containers.", + tips: [ + { + title: 'What are Signals?', + body: 'Probes deployed alongside your containers that observe which code paths are actually used at runtime. This powers the "dynamic" layer of reachability analysis — proving which vulnerabilities are actually reachable in practice.', + }, + { + title: 'Probe health', + body: 'HEALTHY = probe is collecting data normally. DEGRADED = some data collection issues (high latency, missed events). Check the error rate and last fact timestamp.', + }, + { + title: 'Why signals improve security decisions', + body: 'Static analysis says "this code COULD be reached." Signals prove "this code IS being reached." Combined, they dramatically reduce false positives.', + }, + ], + }, + + scripts: { + greeting: "Scripts are reusable automation blocks for deployments, health checks, and maintenance.", + tips: [ + { + title: 'What are scripts?', + body: 'Pre-built automation that runs as part of deployment workflows. Examples: pre-deploy health checks, database migration validators, container size monitors. You can create custom scripts too.', + }, + { + title: 'Script types', + body: 'Bash = shell scripts. Python (py) = Python scripts. JS = Node.js scripts. Each runs in a sandboxed environment with access to Stella APIs for querying release data.', + }, + ], + }, + + diagnostics: { + greeting: "Your first stop when something seems wrong. Doctor runs 100+ health checks across all services.", + tips: [ + { + title: 'How to use Diagnostics', + body: 'Click "Run All Checks" for a full health sweep. Green = healthy, Red = needs attention. Click any failing check for details and remediation steps. Run this after any infrastructure change.', + }, + { + title: 'Common issues', + body: 'Database connectivity, Valkey (cache) availability, feed sync failures, signing key expiration, and service memory pressure. Doctor catches these before they become user-facing issues.', + }, + { + title: 'Pro tip: run daily', + body: 'Set up a daily diagnostic run via Scheduled Jobs. It catches drift, expiring certificates, and stale feeds before they cause problems. Prevention > firefighting.', + }, + ], + }, + + // ========================================================================= + // SETTINGS + // ========================================================================= + integrations: { + greeting: "Integrations connect Stella to your existing tools — registries, SCM, CI, and more.", + tips: [ + { + title: 'Suggested setup order', + body: '1) Registries (where your images live). 2) Source control (Git repos). 3) Scanner config (what to look for). 4) Release controls (environments + approvals). 5) Notifications (Slack, email, webhooks).', + }, + { + title: 'Registry first!', + body: 'Connect your container registry first — Docker Hub, Harbor, GitHub Container Registry, AWS ECR, etc. This lets Stella discover and scan your images automatically.', + action: { label: 'Add integration', route: '/setup/integrations' }, + }, + { + title: 'Zero integrations is normal', + body: 'For a fresh install, no integrations means you haven\'t connected external tools yet. Stella works without them (you can scan manually), but integrations enable automation.', + }, + ], + }, + + 'identity-access': { + greeting: "Manage who can do what. Least privilege + separation of duties = secure operations.", + tips: [ + { + title: 'Built-in roles', + body: 'Admin: full access. Operator: create releases + approve promotions. Viewer: read-only dashboards. Auditor: read + evidence export. Developer: submit scans + view findings.', + }, + { + title: 'Separation of duties', + body: 'For production promotions, the person who CREATES a release should NOT be the same person who APPROVES it. This prevents a single person from pushing unchecked code to production.', + }, + { + title: 'API tokens', + body: 'API tokens enable CI/CD integration. Create scoped tokens with the minimum permissions needed. Rotate regularly and monitor usage in the Audit Log.', + }, + ], + }, + + 'certificates-trust': { + greeting: "Certificates & Trust is the cryptographic backbone — signing keys, trust anchors, and verification.", + tips: [ + { + title: 'Why signing matters', + body: 'Stella signs everything: scans, decisions, evidence, deployments. This proves evidence hasn\'t been tampered with and came from your Stella installation. Without signing, evidence is just data — with signing, it\'s proof.', + }, + { + title: 'Key rotation', + body: 'Rotate signing keys periodically. During transition, both old and new keys remain valid. Old evidence stays verifiable with the old key. Never delete a key while evidence signed by it still needs verification.', + }, + { + title: 'Trusted Issuers', + body: 'External parties whose VEX statements or advisories you trust. Each issuer has a trust score (0-1). Higher trust = more weight in consensus decisions. Manage carefully — trusting a bad source degrades your decisions.', + }, + ], + }, + + 'theme-branding': { + greeting: "Customize how Stella Ops looks for your organization.", + tips: [ + { + title: 'Multi-tenant branding', + body: 'Each tenant can have its own logo, colors, and application title. Useful for MSPs or organizations with multiple business units sharing one Stella installation.', + }, + ], + }, + + 'user-preferences': { + greeting: "Set your personal display preferences — theme, language, layout, and accessibility.", + tips: [ + { + title: 'Layout modes', + body: 'Full-width mode uses all available screen space. Centered mode constrains content to 1400px for readability on ultra-wide monitors. Try both and see what works for your workflow.', + }, + ], + }, + + // ========================================================================= + // DEFAULT (fallback for any unmapped page) + // ========================================================================= + default: { + greeting: "Hi! I'm Stella, your DevOps guide. I can help you understand any page in this platform.", + tips: [ + { + title: 'Need help?', + body: 'Press Ctrl+K to open the command palette and search for anything. Or navigate to the Operations Hub for a prioritized view of what needs your attention.', + action: { label: 'Operations Hub', route: '/ops/operations' }, + }, + { + title: 'Quick orientation', + body: 'Release Control = your deployment pipeline. Security = vulnerability scanning and triage. Evidence = audit-grade proof of every decision. Operations = platform health and jobs. Settings = integrations and users.', + }, + ], + }, + + // Special pages + welcome: { + greeting: "Welcome to Stella Ops! Sign in to start managing governed releases with verifiable evidence.", + tips: [ + { + title: 'What is Stella Ops?', + body: 'A release control plane for container environments that don\'t use Kubernetes. It scans your containers, enforces security policies, manages approvals, and keeps signed proof of every release decision.', + }, + ], + }, + + promotions: { + greeting: "Promotions move releases between environments — with evidence at every step.", + tips: [ + { + title: 'Promotion flow', + body: 'Select a release → choose target environment → Stella evaluates all gates (security scan, policy rules, approvals) → on PASS, the release is deployed → evidence is sealed and signed.', + }, + ], + }, + + 'release-detail': { + greeting: "This is the full detail view for a single release — everything about it in one place.", + tips: [ + { + title: 'Release detail tabs', + body: 'Overview: summary and metadata. Gates: security and policy gate results. Evidence: signed proof bundles. Components: container images in this release. History: promotion and approval timeline.', + }, + ], + }, + + // ========================================================================= + // TAB-LEVEL TIPS — VEX & Exceptions (8 tabs) + // ========================================================================= + 'vex-search': { + greeting: "Search for existing VEX statements across your organization.", + tips: [ + { title: 'How to search', body: 'Search by CVE ID (e.g., CVE-2024-1234), package name, or product name. Results show all VEX statements from all sources — your team, vendors, and community.' }, + { title: 'Statement sources', body: 'Statements come from: your team (manual creation), upstream vendors (imported), scanner analysis (auto-generated), and community databases. Each carries a trust score.' }, + ], + }, + 'vex-create': { + greeting: "Create a new VEX statement — document why a vulnerability does or doesn't affect you.", + tips: [ + { title: 'Creating a VEX statement', body: 'Select the vulnerability (CVE), choose your product/component, set the status (Affected/Not Affected/Fixed/Under Investigation), and provide justification. The statement is cryptographically signed automatically.' }, + { title: 'Justification is key', body: 'A good justification explains WHY, not just WHAT. "Not affected because the vulnerable function requires TLS 1.0 which we disabled in config" is better than "Not affected — reviewed."' }, + { title: 'Impact of your statement', body: 'Once created, your VEX statement automatically suppresses the finding in future scans. It also feeds into VEX consensus for the broader community if you choose to share.' }, + ], + }, + 'vex-stats': { + greeting: "VEX statistics show the health and coverage of your exploitability assessments.", + tips: [ + { title: 'What to look for', body: 'High "Not Affected" count = you\'ve documented why most findings are noise. High "Investigating" = assessment backlog. Zero "Affected" is rare — if you see it, check whether your team is actually triaging.' }, + { title: 'Coverage trend', body: 'VEX coverage should increase over time as your team documents more findings. Flat or declining coverage may indicate the team has stopped triaging, which increases risk.' }, + ], + }, + 'vex-consensus': { + greeting: "Consensus shows how multiple VEX sources agree (or disagree) about vulnerabilities.", + tips: [ + { title: 'How consensus works', body: 'When multiple sources publish VEX statements about the same vulnerability, Stella weighs them by trust score. A vendor saying "not affected" (trust: 0.9) outweighs a community report (trust: 0.5). The effective status is the trust-weighted result.' }, + { title: 'Conflicts', body: 'When sources disagree (vendor says "not affected", scanner says "affected"), a conflict is raised. Review conflicts in the Conflicts tab — they require human judgment to resolve.' }, + { title: 'Trust scores', body: 'Each VEX issuer has a trust score (0-1) based on: Authority (are they the vendor?), Accuracy (historical correctness), Timeliness (how fast they respond), Verification (do they provide evidence?).' }, + ], + }, + 'vex-explorer': { + greeting: "The VEX Explorer lets you browse raw VEX data across all sources.", + tips: [ + { title: 'Raw vs effective', body: 'This view shows raw VEX statements as received — unmodified by consensus or policy. Use this to audit what each source actually said, trace provenance, and verify statement integrity.' }, + ], + }, + 'vex-conflicts': { + greeting: "VEX conflicts occur when sources disagree about a vulnerability's impact.", + tips: [ + { title: 'What creates a conflict?', body: 'Vendor says "not affected" but your scanner detects reachable code paths. Or two vendors publish contradictory statements. Conflicts require human review — they can\'t be auto-resolved.' }, + { title: 'Resolving conflicts', body: 'Review the evidence from each source. Check reachability data. Consider trust scores. Then either: override with your own VEX statement, adjust trust weights, or escalate for expert review.' }, + ], + }, + 'vex-exceptions': { + greeting: "Exceptions are temporary policy overrides — accepted risk with an expiration date.", + tips: [ + { title: 'What is a policy exception?', body: 'When a vulnerability is real and affects you, but you can\'t fix it right now, create an exception. It documents: what risk you\'re accepting, why, who approved it, and when it expires. Think of it as a time-limited "known issue."' }, + { title: 'Exception lifecycle', body: 'Create → requires approval (separation of duties) → Active until expiry → Expired (finding re-surfaces). Exceptions are auditable evidence — auditors can see exactly what was accepted and when.' }, + { title: 'Approval requirements', body: 'Exceptions typically require approval from someone with a higher role than the requester. Critical exceptions may need multiple approvers. This prevents individuals from silently accepting major risks.' }, + ], + }, + 'exception-approvals': { + greeting: "Exception approvals enforce separation of duties for risk acceptance.", + tips: [ + { title: 'Your approval queue', body: 'Pending exceptions await your review. For each: check the vulnerability severity, read the justification, review the proposed expiration, then Approve or Reject with your reasoning.' }, + ], + }, + 'vex-statement-detail': { + greeting: "Full details of a single VEX statement — provenance, justification, and evidence.", + tips: [ + { title: 'Verifying a statement', body: 'Check: Who issued it? What trust score do they have? Is the justification specific or vague? Does the evidence (reachability, config) support the claim? Statements without evidence should be treated with lower confidence.' }, + ], + }, + + // ========================================================================= + // TAB-LEVEL TIPS — Policy Governance (tabs) + // ========================================================================= + 'governance-budget': { + greeting: "The risk budget dashboard shows how much of your allowed risk capacity is consumed.", + tips: [ + { title: 'Reading the budget chart', body: 'The trend chart shows budget consumption over time. Spikes indicate new vulnerabilities or expired exceptions. A steadily rising trend means security debt is accumulating faster than you\'re remediating.' }, + { title: 'Budget thresholds', body: '70% = warning (advisory, logged). 90% = critical (can block promotions). 100% = hard stop (no promotions until budget is reduced). Thresholds are configurable per environment.', contextTrigger: 'budget-exceeded' }, + { title: 'Top contributors', body: 'Focus remediation on the top contributors — fixing one high-impact issue often reduces the budget more than fixing ten low-impact ones. The chart shows which CVEs or components consume the most budget.' }, + ], + }, + 'governance-budget-config': { + greeting: "Configure risk budget thresholds, scoring weights, and environment-specific overrides.", + tips: [ + { title: 'Tuning your budget', body: 'Start conservative (lower thresholds) and loosen as your team builds VEX coverage. Too tight = constant false blocks. Too loose = meaningless. The budget should accurately reflect organizational risk tolerance.' }, + ], + }, + 'governance-profiles': { + greeting: "Risk profiles define different risk tolerances for different contexts.", + tips: [ + { title: 'Why profiles exist', body: 'Production needs tight controls. Dev can be looser. A "SOC 2 Prod" profile might allow zero critical CVEs, while a "Dev Fast" profile allows highs with justification. Profiles map to environments.' }, + ], + }, + 'governance-profile-new': { + greeting: "Create a new risk profile — define the security boundaries for an environment type.", + tips: [ + { title: 'Profile design tips', body: 'Start from an existing profile and modify. Key decisions: max severity allowed, reachability requirements (require proof?), VEX coverage minimum, approval count, and exception policies.' }, + ], + }, + 'governance-conflicts': { + greeting: "Policy conflicts occur when rules contradict each other.", + tips: [ + { title: 'What causes policy conflicts?', body: 'Two packs defining different severity thresholds for the same environment. Or a global rule that contradicts an environment-specific override. Stella detects these automatically.' }, + { title: 'Resolution strategies', body: 'Option 1: Merge rules (most restrictive wins). Option 2: Add priority/precedence. Option 3: Remove the conflicting rule. The resolution wizard guides you through each option.' }, + ], + }, + 'governance-trust-weights': { + greeting: "Trust weights determine how much influence each VEX source has in consensus decisions.", + tips: [ + { title: 'Setting trust weights', body: 'Vendor statements (they know their code best) typically get 0.8-0.9. Your own team assessments: 0.7-0.9 (depending on expertise). Community reports: 0.3-0.5. Scanner auto-generated: 0.4-0.6.' }, + ], + }, + 'governance-staleness': { + greeting: "Staleness policies control how old advisory data can be before it blocks operations.", + tips: [ + { title: 'Why staleness matters', body: 'If your NVD feed is 7 days old, you might miss critical CVEs published yesterday. The staleness budget defines the max acceptable age before promotions are blocked or warnings are raised.' }, + { title: 'Air-gap consideration', body: 'In air-gapped environments, feeds can\'t auto-sync. Set realistic staleness budgets — typically 7-14 days for air-gap, with mandatory manual updates during the window.' }, + ], + }, + 'governance-sealed-mode': { + greeting: "Sealed mode locks down policy changes — for production stability or compliance freezes.", + tips: [ + { title: 'What sealed mode does', body: 'When sealed, no policy changes can be made without explicit override. Use during: production release windows, compliance audit periods, or when you want to freeze the security posture.' }, + ], + }, + 'governance-validator': { + greeting: "Validate your policy rules before activating them.", + tips: [ + { title: 'How validation works', body: 'The validator checks your rules for: syntax errors, logical contradictions, unreachable conditions, and potential false-positive rates. Fix issues here before deploying rules to production.' }, + ], + }, + 'governance-impact': { + greeting: "Impact preview shows what would happen if you activated proposed policy changes.", + tips: [ + { title: 'Using impact preview', body: 'Before activating new rules, see how many existing releases would be affected. A rule that blocks 80% of current releases is probably too aggressive. Aim for targeted impact.' }, + ], + }, + 'governance-schema-playground': { + greeting: "The schema playground lets you experiment with policy rule syntax interactively.", + tips: [ + { title: 'Learning Stella DSL', body: 'Stella DSL is the policy language. Try rules like: `deny if severity == "critical" and reachability > 0.7` or `require approval_count >= 2 when environment == "production"`. The playground shows results instantly.' }, + ], + }, + 'governance-schema-docs': { + greeting: "Schema documentation for the Stella policy DSL.", + tips: [ + { title: 'Key schema concepts', body: 'Inputs: severity, reachability, vex_status, environment, component. Operators: ==, !=, >, <, in, contains. Actions: deny, warn, require, allow. Conditions: when, if, unless.' }, + ], + }, + + // ========================================================================= + // TAB-LEVEL TIPS — Policy Simulation (6 tabs) + // ========================================================================= + 'sim-shadow': { + greeting: "Shadow mode runs proposed rules alongside active ones — no impact on real releases.", + tips: [ + { title: 'How shadow mode works', body: 'Enable shadow mode, then every real promotion is evaluated twice: once with active rules (real outcome) and once with proposed rules (shadow outcome). Differences are highlighted so you can see the impact before going live.' }, + ], + }, + 'sim-promotion-gate': { + greeting: "Simulate how a specific promotion would be evaluated by your policy gates.", + tips: [ + { title: 'Gate simulation', body: 'Select a release and target environment, then see exactly which gates would pass, warn, or block. This answers: "If I promote this right now, what would happen?" without actually promoting.' }, + ], + }, + 'sim-test-validate': { + greeting: "Test your policy rules against sample data to verify they work as intended.", + tips: [ + { title: 'Writing test cases', body: 'Create sample inputs (mock release with known CVEs, specific severity levels) and verify your rules produce the expected outcome. Like unit tests for your security policy.' }, + ], + }, + 'sim-pre-promotion': { + greeting: "Pre-promotion review shows the policy evaluation a release would face before you request it.", + tips: [ + { title: 'Why pre-check', body: 'Avoid failed promotions by checking first. This view shows all gates, their current state, and what evidence is still needed. Fix issues before requesting the promotion.' }, + ], + }, + 'sim-effective': { + greeting: "Effective policies show the final merged result of all active packs and overrides.", + tips: [ + { title: 'Why this matters', body: 'When you have multiple policy packs plus environment overrides plus exceptions, it\'s hard to know what the ACTUAL rules are. This view flattens everything into the final effective policy.' }, + ], + }, + 'sim-exceptions': { + greeting: "See how active exceptions modify the effective policy.", + tips: [ + { title: 'Exception impact', body: 'Each active exception creates a "hole" in your policy. This view shows exactly what\'s being exempted, by whom, until when, and what the policy would say without the exception.' }, + ], + }, + + // ========================================================================= + // TAB-LEVEL TIPS — Audit Log (sub-routes) + // ========================================================================= + 'audit-log-events': { + greeting: "The full event log — every platform action, searchable and filterable.", + tips: [ + { title: 'Searching events', body: 'Filter by: module (releases, policy, identity), action type (create, approve, delete), actor (who did it), timestamp range, and resource ID. Use this for incident investigation or compliance audits.' }, + { title: 'Export for compliance', body: 'Export filtered events as JSON or CSV for external audit tools. Exported data includes cryptographic proof that events weren\'t modified after creation.' }, + ], + }, + 'audit-log-export': { + greeting: "Export audit data for external compliance tools or regulatory submissions.", + tips: [ + { title: 'Export formats', body: 'JSON (structured, machine-readable), CSV (spreadsheet-friendly), or StellBundle (signed, verifiable). Choose based on your auditor\'s requirements.' }, + ], + }, + 'policy-audit-vex': { + greeting: "VEX audit trail — every VEX statement created, modified, or revoked.", + tips: [ + { title: 'VEX audit events', body: 'Track: who created each statement, what justification was given, whether consensus changed, and if any conflicts were resolved. Essential for proving your VEX process to auditors.' }, + ], + }, + 'policy-audit-events': { + greeting: "All policy-related events in chronological order.", + tips: [ + { title: 'Key events to watch', body: 'Policy pack activation/deactivation, rule changes, budget threshold breaches, exception approvals/rejections, and sealed mode toggles. Set alerts for unexpected off-hours changes.' }, + ], + }, + + // ========================================================================= + // TAB-LEVEL TIPS — Certificates & Trust (3 tabs) + // ========================================================================= + 'trust-issuers': { + greeting: "Trusted issuers are external parties whose VEX statements and advisories you trust.", + tips: [ + { title: 'Managing trust', body: 'Each issuer has a composite trust score (0-1) based on: Authority (0-1), Accuracy (0-1), Timeliness (0-1), Verification (0-1). Only add issuers you\'ve vetted. A compromised issuer could inject false "not affected" statements.' }, + { title: 'Common issuers', body: 'Software vendors (highest trust for their own products), CERT organizations, scanner vendors, and community databases. Each should be configured with appropriate trust boundaries.' }, + ], + }, + + // ========================================================================= + // OPERATIONS — deeper sub-pages + // ========================================================================= + 'offline-kit': { + greeting: "The Offline Kit packages everything needed for air-gapped deployment.", + tips: [ + { title: 'What\'s in an offline kit?', body: 'Vulnerability feeds (frozen snapshot), Stella container images, scanner analyzers, CLI tools, trust roots for signature verification, and database snapshots. Everything an isolated installation needs.' }, + { title: 'Delta updates', body: 'After the initial kit, daily deltas are much smaller (<350MB). Transfer via USB, sneakernet, or approved data diode. The staleness budget controls how often you must update.' }, + ], + }, + 'dead-letter': { + greeting: "The dead-letter queue holds failed jobs for investigation and retry.", + tips: [ + { title: 'Why jobs fail', body: 'Common causes: target host unreachable, out of disk space, image pull failure, timeout, or policy evaluation error. Each dead letter has full error details and retry history.' }, + { title: 'Don\'t let them pile up', body: 'A growing dead-letter queue often signals infrastructure problems. Investigate root causes rather than just retrying — the same failure will likely repeat.' }, + ], + }, + 'aoc-compliance': { + greeting: "AOC (Aggregation-Only Contract) compliance checks verify data integrity boundaries.", + tips: [ + { title: 'What is AOC?', body: 'AOC ensures that ingestion services (Concelier, Excititor) only store raw facts — never computed severities, consensus, or policy hints. The Policy Engine does all derivation. AOC checks verify this boundary is respected.' }, + ], + }, + 'health-slo': { + greeting: "SLO monitoring tracks service-level objectives across the platform.", + tips: [ + { title: 'Key SLOs', body: 'Scan latency (p95 under 5 minutes), promotion gate evaluation (under 30 seconds), feed sync freshness (under 6 hours), and evidence signing (under 2 seconds). Breaches indicate capacity or infrastructure issues.' }, + ], + }, + 'watchlist': { + greeting: "The watchlist lets you track specific CVEs, components, or releases for changes.", + tips: [ + { title: 'Setting up watches', body: 'Watch a CVE to be notified when new VEX statements or advisories are published. Watch a component to track vulnerability changes. Watch a release to monitor gate status changes.' }, + ], + }, + 'runtime-drift': { + greeting: "Drift detection catches when deployed containers don't match what was released.", + tips: [ + { title: 'What is drift?', body: 'Drift occurs when the actual container running on a host differs from what Stella last promoted. Causes: manual docker pull, tag mutation, unauthorized deployments. Drift is a security risk — it bypasses your policy gates.' }, + ], + }, + 'event-stream': { + greeting: "The real-time event stream shows platform events as they happen.", + tips: [ + { title: 'Using the stream', body: 'Watch for: promotion approvals, scan completions, policy violations, and feed sync events in real-time. Useful during deployment windows or incident response to see what\'s happening right now.' }, + ], + }, + 'ai-runs': { + greeting: "AI runs show Advisory AI analysis history and results.", + tips: [ + { title: 'What Advisory AI does', body: 'Advisory AI helps analyze vulnerabilities, suggest remediations, and draft VEX justifications. Each run is logged here with inputs, outputs, and confidence scores.' }, + ], + }, + + // ========================================================================= + // TRIAGE & WORKSPACE — deep context + // ========================================================================= + 'triage-workspace': { + greeting: "The triage workspace gives you everything needed to make a decision about this artifact.", + tips: [ + { title: 'Workspace tabs', body: 'Evidence: unified view of all evidence for selected finding. Overview: finding summary. Reachability: can this vuln be reached? Policy: what do rules say? Delta: what changed from baseline? Attestations: signed proofs.' }, + { title: 'Making a triage decision', body: 'For each finding: 1) Check severity and reachability. 2) Read the advisory. 3) Check if a VEX statement exists. 4) Decide: fix, accept risk (create exception), or mark not-applicable (create VEX). 5) Your decision becomes evidence.' }, + { title: 'The Delta tab', body: 'Delta shows what\'s NEW compared to a baseline. A "!" badge means changes were detected. Focus on delta findings — they represent regression from your known-good state.' }, + ], + }, + + // ========================================================================= + // RELEASE CONTROL — deeper pages + // ========================================================================= + approvals: { + greeting: "The approval queue shows all releases waiting for human sign-off.", + tips: [ + { title: 'Approval decision cockpit', body: 'Click any approval to open the full decision cockpit with tabs: Overview, Gates, Security, Reachability, Ops/Data, Evidence, Replay/Verify, History. Everything you need to decide is right here.' }, + { title: 'Separation of duties', body: 'You can\'t approve your own releases. The system enforces that the creator and approver are different people. This prevents a single person from pushing unchecked code to production.' }, + ], + }, + 'approval-detail': { + greeting: "Full decision cockpit — all context needed to approve or reject this promotion.", + tips: [ + { title: 'What to check before approving', body: '1) Gates tab: all gates should be PASS or WARN (not BLOCK). 2) Security tab: review open vulnerabilities and VEX coverage. 3) Reachability: are criticals reachable? 4) Evidence: is the proof chain complete?' }, + { title: 'Replay/Verify tab', body: 'This tab lets you deterministically replay the policy evaluation. If you get the same result as the automated evaluation, you know the decision wasn\'t tampered with.' }, + ], + }, + hotfixes: { + greeting: "Hotfixes bypass the normal promotion flow for emergency production fixes.", + tips: [ + { title: 'When to use hotfixes', body: 'Only for production emergencies that can\'t wait for the normal Dev → Stage → Prod flow. Hotfixes still go through gates, but may have relaxed approval requirements and faster timeouts.' }, + { title: 'Hotfix audit trail', body: 'Every hotfix is flagged in the audit log. If your organization has SLA requirements for emergency change documentation, the evidence is automatically captured.' }, + ], + }, + 'investigation-timeline': { + greeting: "The investigation timeline helps you trace what happened during a deployment incident.", + tips: [ + { title: 'Incident investigation', body: 'See the chronological sequence of events: when was the release created, who approved it, what gates evaluated, when did deployment start, what errors occurred. Essential for post-mortem analysis.' }, + ], + }, + 'deploy-diff': { + greeting: "Deploy diff shows exactly what changed between two deployments.", + tips: [ + { title: 'Using deploy diff', body: 'Compare the current deployment against the previous one. See: new container images, changed configurations, added/removed components, and vulnerability delta. Answers: "What\'s different about this deploy?"' }, + ], + }, + 'change-trace': { + greeting: "Change trace follows the full lineage of a change from commit to deployment.", + tips: [ + { title: 'End-to-end traceability', body: 'From git commit → CI build → image push → scan → release → promotion → deployment. Every step has evidence. If something went wrong, trace backwards to find where.' }, + ], + }, + 'releases-versions': { + greeting: "Versions track the complete release history for a component.", + tips: [ + { title: 'Versions vs Releases', body: 'A Version is a specific build of a component (e.g., api-gateway v2.1.0). A Release bundles one or more versions together for deployment. Think: versions = ingredients, release = the recipe.' }, + ], + }, + 'promotion-graph': { + greeting: "The promotion graph visualizes all possible promotion paths across environments.", + tips: [ + { title: 'Reading the graph', body: 'Nodes are environments. Edges are allowed promotion paths. Edge labels show gate requirements. Thick edges = frequently used paths. Red edges = currently blocked paths.' }, + ], + }, + + // ========================================================================= + // EVIDENCE — deeper pages + // ========================================================================= + 'evidence-replay': { + greeting: "Replay & Verify lets you re-run past decisions to prove they were correct.", + tips: [ + { title: 'Deterministic replay', body: 'Feed the same frozen inputs (SBOM, feeds, policy version) through the same evaluation engine. If you get the same output, the original decision is proven correct and untampered.' }, + { title: 'When to replay', body: 'During audits ("prove this release was properly evaluated"), incident response ("was the gate evaluation correct at the time?"), or trust verification ("has evidence been tampered with?").' }, + ], + }, + 'evidence-proofs': { + greeting: "Proof chains link evidence across the entire release lifecycle.", + tips: [ + { title: 'What is a proof chain?', body: 'A chain of cryptographic hashes linking: scan evidence → policy evaluation → approval record → deployment evidence. If any link is modified, the chain breaks. This proves end-to-end integrity.' }, + ], + }, + 'evidence-threads': { + greeting: "Evidence threads group all evidence for a specific package or component.", + tips: [ + { title: 'Thread lookup', body: 'Search by package URL (purl) to see every scan, VEX statement, policy decision, and attestation related to that package across all releases and environments.' }, + ], + }, + 'workspace-auditor': { + greeting: "The auditor workspace is optimized for compliance review — evidence-first view.", + tips: [ + { title: 'Auditor lens', body: 'This view prioritizes: signed evidence bundles, proof chain verification, policy compliance status, and exportable artifacts. Designed for external auditors who need to verify your security posture.' }, + ], + }, + 'workspace-developer': { + greeting: "The developer workspace focuses on remediation — what to fix and how.", + tips: [ + { title: 'Developer lens', body: 'This view prioritizes: vulnerability details, affected code paths, fix recommendations, upgrade paths, and patch availability. Designed for developers who need to remediate findings.' }, + ], + }, + + // ========================================================================= + // SETTINGS — deeper pages + // ========================================================================= + 'identity-providers': { + greeting: "Configure external identity providers for SSO integration.", + tips: [ + { title: 'SSO setup', body: 'Connect your organization\'s identity provider (Azure AD, Okta, Keycloak, etc.) so users can sign in with existing corporate credentials. Supports OIDC and SAML protocols.' }, + ], + }, + 'notification-settings': { + greeting: "Configure how and when Stella sends notifications.", + tips: [ + { title: 'Notification channels', body: 'Email: for approval requests and audit events. Slack/Teams: for real-time scan results and promotion status. Webhooks: for CI/CD integration and custom automation.' }, + ], + }, + 'usage-limits': { + greeting: "Usage & limits shows your plan consumption and operational quotas.", + tips: [ + { title: 'Plan limits', body: 'Free: 3 environments, 999 scans/month. Plus: 33 environments, 9,999 scans. Pro: 333 environments, 99,999 scans. Business: 3,333 environments, 999,999 scans. All tiers include all features.' }, + ], + }, + 'ai-preferences': { + greeting: "Configure Advisory AI behavior and preferences.", + tips: [ + { title: 'AI in Stella', body: 'Advisory AI assists with: vulnerability analysis, remediation suggestions, VEX statement drafting, and pattern detection. It\'s advisory-only — all decisions still require human approval.' }, + ], + }, + 'scanner-ops': { + greeting: "Scanner operations let you configure scan behavior and analyzer settings.", + tips: [ + { title: 'Scanner config', body: 'Configure: which language analyzers to enable (Node, Go, .NET, Python, etc.), secret detection rules, binary analysis depth, and SBOM output formats. Changes apply to all future scans.' }, + ], + }, + + // ========================================================================= + // ADMIN pages + // ========================================================================= + 'admin-tenants': { + greeting: "Tenant management for multi-tenant Stella installations.", + tips: [ + { title: 'Multi-tenancy', body: 'Each tenant is a fully isolated workspace with its own users, policies, releases, and evidence. Tenants can\'t see each other\'s data. Use for: separate business units, client isolation, or dev/prod separation.' }, + ], + }, + 'admin-roles': { + greeting: "Define roles and their associated permission scopes.", + tips: [ + { title: 'Scope-based permissions', body: 'Roles are collections of scopes (e.g., release:read, policy:approve, signer:sign). Built-in roles cover common patterns. Custom roles let you create fine-grained access for specific workflows.' }, + ], + }, + 'admin-clients': { + greeting: "OAuth clients enable service-to-service and CI/CD authentication.", + tips: [ + { title: 'Client types', body: 'Confidential clients: for backend services that can keep a secret. Public clients: for CLI tools and SPAs. Each client gets specific scopes — never grant more than needed.' }, + ], + }, + 'admin-tokens': { + greeting: "API tokens for programmatic access and CI/CD integration.", + tips: [ + { title: 'Token best practices', body: 'Create scoped tokens with minimum required permissions. Set short expiration (30-90 days). Rotate on schedule. Monitor usage in audit logs. Revoke immediately if compromised.' }, + ], + }, + + // ========================================================================= + // CONTEXT-TRIGGERED TIPS (shown when page components push context) + // These are available on ANY page that pushes the matching context key. + // ========================================================================= + // Note: Context-triggered tips are stored in the page where they're most + // likely to appear, but the StellaHelperContextService can inject them + // on any page. The contextTrigger field is used for matching. + // See the dashboard tips for examples with contextTrigger field. +}; diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper.component.ts new file mode 100644 index 000000000..d093a143f --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-helper.component.ts @@ -0,0 +1,1346 @@ +import { + Component, + ChangeDetectionStrategy, + signal, + computed, + effect, + inject, + OnInit, + OnDestroy, +} from '@angular/core'; +import { SlicePipe } from '@angular/common'; +import { Router, NavigationEnd } from '@angular/router'; +import { filter } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; +import { StellaAssistantService } from './stella-assistant.service'; +import { StellaHelperContextService } from './stella-helper-context.service'; +import { SearchAssistantDrawerService } from '../../../core/services/search-assistant-drawer.service'; +import { + StellaHelperTip, + StellaHelperPageConfig, + PAGE_TIPS, + resolvePageKey, +} from './stella-helper-tips.config'; + +const STORAGE_KEY = 'stellaops.helper.preferences'; +const IDLE_ANIMATION_INTERVAL = 12_000; // wiggle every 12s +const AUTO_TIP_INTERVAL = 45_000; // suggest next tip every 45s + +interface HelperPreferences { + dismissed: boolean; + seenPages: string[]; + tipIndex: Record; +} + +const DEFAULTS: HelperPreferences = { + dismissed: false, + seenPages: [], + tipIndex: {}, +}; + +/** + * StellaHelperComponent — Clippy-style contextual help assistant. + * + * A fixed-position animated mascot (Stella star) that sits in the + * bottom-right corner and provides page-aware tips and guidance. + * + * Designed for onboarding new DevOps engineers who may not know + * domain concepts like SBOM, VEX, reachability, policy gates, etc. + * + * Features: + * - Page-aware tips (changes content on navigation) + * - Animated idle states (bounce, wiggle, peek) + * - Speech bubble with tip cycling + * - Dismissable per-user (persisted to localStorage) + * - "Minimize" to just the mascot (click to re-open) + * - Action buttons linking to relevant pages + * - First-visit greeting per page + */ +@Component({ + selector: 'app-stella-helper', + standalone: true, + imports: [SlicePipe], + template: ` + + @if (prefs().dismissed && !forceShow()) { + + } + + + @if (!prefs().dismissed || forceShow()) { + + } + `, + styles: [` + /* ===== Restore button (when dismissed) ===== */ + .helper-restore { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 750; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: 1px solid var(--color-border-primary); + border-radius: 50%; + background: var(--color-surface-primary); + cursor: pointer; + box-shadow: var(--shadow-md); + transition: transform 0.2s, box-shadow 0.2s; + + &:hover { + transform: scale(1.1); + box-shadow: var(--shadow-lg); + } + } + + .helper-restore__img { + border-radius: 50%; + } + + .helper-restore__badge { + position: absolute; + top: -4px; + right: -4px; + width: 18px; + height: 18px; + border-radius: 50%; + background: var(--color-brand-primary); + color: var(--color-text-heading); + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + line-height: 1; + } + + /* ===== Main helper container ===== */ + .helper { + position: fixed; + bottom: 24px; + right: 24px; + z-index: 750; + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; + pointer-events: none; + + & > * { + pointer-events: auto; + } + } + + /* ===== Expanded mode (search/chat) ===== */ + .helper--expanded .helper__bubble { + width: 420px; + max-height: 70vh; + } + + .helper--expanded .helper__chat-messages, + .helper--expanded .helper__chat-streaming { + max-height: 340px; + } + + .helper--expanded .helper__search-results { + max-height: 320px; + } + + /* ===== Entrance animation ===== */ + .helper--entering .helper__mascot { + animation: helper-enter 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both; + } + + @keyframes helper-enter { + 0% { + opacity: 0; + transform: translateY(80px) scale(0.3) rotate(-20deg); + } + 60% { + opacity: 1; + transform: translateY(-8px) scale(1.1) rotate(5deg); + } + 100% { + transform: translateY(0) scale(1) rotate(0deg); + } + } + + /* ===== Idle animations ===== */ + .helper--idle-bounce .helper__mascot-img { + animation: helper-bounce 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + @keyframes helper-bounce { + 0%, 100% { transform: translateY(0); } + 40% { transform: translateY(-12px); } + 60% { transform: translateY(-4px); } + } + + .helper--idle-wiggle .helper__mascot-img { + animation: helper-wiggle 0.5s ease-in-out; + } + + @keyframes helper-wiggle { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(-12deg); } + 75% { transform: rotate(12deg); } + } + + .helper--idle-peek .helper__mascot-img { + animation: helper-peek 0.8s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + @keyframes helper-peek { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 30% { transform: translateY(-6px) rotate(-8deg); } + 60% { transform: translateY(-3px) rotate(4deg); } + } + + /* ===== Mascot button ===== */ + .helper__mascot { + position: relative; + width: 56px; + height: 56px; + border: 2px solid var(--color-brand-primary); + border-radius: 50%; + background: var(--color-surface-primary); + cursor: pointer; + box-shadow: + 0 4px 16px rgba(245, 166, 35, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 0.2s; + padding: 0; + overflow: visible; + + &:hover { + transform: scale(1.1); + box-shadow: + 0 6px 24px rgba(245, 166, 35, 0.35), + 0 4px 12px rgba(0, 0, 0, 0.15); + } + + &:active { + transform: scale(0.95); + } + } + + .helper__mascot-img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; + display: block; + } + + /* Attention pulse ring */ + .helper__mascot-pulse { + position: absolute; + inset: -4px; + border-radius: 50%; + border: 2px solid var(--color-brand-primary); + animation: helper-pulse 2.5s ease-in-out infinite; + pointer-events: none; + } + + @keyframes helper-pulse { + 0%, 100% { opacity: 0; transform: scale(1); } + 50% { opacity: 0.6; transform: scale(1.15); } + } + + /* ===== Speech bubble ===== */ + .helper__bubble { + position: relative; + width: 320px; + max-width: calc(100vw - 80px); + background: var(--color-surface-primary); + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-xl, 12px); + box-shadow: + 0 8px 32px rgba(0, 0, 0, 0.12), + 0 2px 8px rgba(0, 0, 0, 0.06); + animation: bubble-enter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both; + overflow: hidden; + } + + @keyframes bubble-enter { + 0% { + opacity: 0; + transform: translateY(12px) scale(0.9); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + } + } + + .helper__bubble-close { + position: absolute; + top: 6px; + right: 6px; + width: 26px; + height: 26px; + border: 1px solid transparent; + border-radius: 50%; + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + z-index: 2; + + &:hover { + background: color-mix(in srgb, var(--color-status-error, #c62828) 10%, transparent); + border-color: color-mix(in srgb, var(--color-status-error, #c62828) 30%, transparent); + color: var(--color-status-error, #c62828); + } + } + + .helper__bubble-content { + padding: 14px 16px 10px; + } + + .helper__tip-title { + font-size: 0.8125rem; + font-weight: 700; + color: var(--color-text-heading); + margin-bottom: 6px; + padding-right: 20px; + line-height: 1.3; + /* Subtle left accent bar for visual hierarchy */ + border-left: 3px solid var(--color-brand-primary); + padding-left: 8px; + margin-left: -2px; + } + + .helper__tip-body { + font-size: 0.75rem; + line-height: 1.6; + color: var(--color-text-primary); + padding-left: 9px; + } + + .helper__tip-action { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 10px; + margin-left: 9px; + padding: 5px 12px; + border: 1px solid var(--color-brand-primary); + border-radius: var(--radius-full, 9999px); + background: transparent; + color: var(--color-brand-primary); + font-size: 0.6875rem; + font-weight: 600; + cursor: pointer; + transition: background 0.2s, color 0.2s, transform 0.15s; + + &:hover { + background: var(--color-brand-primary); + color: var(--color-text-heading); + transform: translateX(2px); + } + } + + /* Tip navigation */ + .helper__tip-nav { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 6px 16px; + border-top: 1px solid color-mix(in srgb, var(--color-border-primary) 40%, transparent); + } + + .helper__tip-nav-btn { + width: 24px; + height: 24px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background 0.15s, color 0.15s; + + &:hover:not(:disabled) { + background: var(--color-surface-tertiary); + color: var(--color-text-primary); + } + + &:disabled { + opacity: 0.3; + cursor: default; + } + } + + .helper__tip-counter { + font-size: 0.6875rem; + color: var(--color-text-secondary); + font-variant-numeric: tabular-nums; + min-width: 40px; + text-align: center; + } + + /* Crossfade wrapper for tips ↔ search transition */ + .helper__crossfade-wrap { + position: relative; + overflow: hidden; + min-height: 60px; + } + + .helper__crossfade-panel { + transition: opacity 200ms ease, transform 200ms ease; + } + + .helper__crossfade--active { + opacity: 1; + transform: translateY(0); + position: relative; + z-index: 1; + pointer-events: auto; + } + + .helper__crossfade--out { + opacity: 0; + transform: translateY(-8px); + position: absolute; + inset: 0; + z-index: 0; + pointer-events: none; + } + + /* Search results */ + .helper__search-loading { + font-size: 0.6875rem; + color: var(--color-brand-primary); + font-weight: 600; + animation: stage-pulse 1.5s ease-in-out infinite; + padding: 4px 0; + } + + .helper__search-synthesis { + font-size: 0.6875rem; + line-height: 1.5; + color: var(--color-text-primary); + padding: 6px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-primary) 30%, transparent); + margin-bottom: 6px; + } + + .helper__search-results { + display: flex; + flex-direction: column; + gap: 3px; + max-height: 200px; + overflow-y: auto; + } + + .helper__search-card { + display: flex; + flex-direction: column; + gap: 2px; + padding: 7px 10px; + border: 1px solid transparent; + border-radius: var(--radius-md, 8px); + background: transparent; + cursor: pointer; + text-align: left; + transition: background 0.15s, border-color 0.15s, transform 0.12s; + + &:hover { + background: var(--color-surface-secondary); + border-color: color-mix(in srgb, var(--color-brand-primary) 25%, transparent); + transform: translateX(2px); + } + + &:active { + transform: translateX(0) scale(0.99); + } + } + + .helper__search-card-type { + font-size: 0.5rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--color-brand-primary); + display: inline-flex; + align-items: center; + gap: 4px; + + &::before { + content: ''; + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--color-brand-primary); + opacity: 0.5; + } + } + + .helper__search-card-title { + font-size: 0.75rem; + font-weight: 600; + color: var(--color-text-heading); + line-height: 1.3; + } + + .helper__search-card-snippet { + font-size: 0.625rem; + color: var(--color-text-secondary); + line-height: 1.4; + } + + /* Chat error */ + .helper__chat-error { + font-size: 0.6875rem; + color: var(--color-status-error, #c62828); + padding: 8px; + border-radius: var(--radius-sm, 4px); + background: color-mix(in srgb, var(--color-status-error, #c62828) 8%, transparent); + } + + .helper__chat-citations { + display: inline-block; + font-size: 0.5625rem; + color: var(--color-brand-primary); + margin-top: 2px; + } + + /* Search results pager */ + .helper__search-pager { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 6px 0 2px; + border-top: 1px solid color-mix(in srgb, var(--color-border-primary) 25%, transparent); + } + + .helper__search-total { + font-size: 0.5625rem; + color: var(--color-text-secondary); + margin-left: 4px; + } + + /* Search/chat input bar */ + .helper__input-bar { + display: flex; + align-items: center; + gap: 4px; + padding: 6px 12px; + border-top: 1px solid color-mix(in srgb, var(--color-border-primary) 40%, transparent); + } + + .helper__input { + flex: 1; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-full, 9999px); + padding: 6px 14px; + font-size: 0.6875rem; + background: var(--color-surface-secondary); + color: var(--color-text-primary); + outline: none; + transition: border-color 0.2s, box-shadow 0.2s, background 0.2s; + + &::placeholder { + color: var(--color-text-secondary); + font-style: italic; + } + + &:focus { + border-color: var(--color-brand-primary); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-brand-primary) 15%, transparent); + background: var(--color-surface-primary); + } + } + + .helper__input-send { + width: 28px; + height: 28px; + border: none; + border-radius: 50%; + background: var(--color-brand-primary); + color: var(--color-text-heading); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: opacity 0.15s, transform 0.15s; + + &:disabled { + opacity: 0.3; + cursor: default; + } + + &:hover:not(:disabled) { + transform: scale(1.1); + } + } + + /* Mode back button */ + .helper__mode-back { + float: right; + display: inline-flex; + align-items: center; + gap: 2px; + border: none; + background: transparent; + color: var(--color-text-secondary); + font-size: 0.625rem; + cursor: pointer; + + &:hover { + color: var(--color-brand-primary); + } + } + + /* Chat messages */ + .helper__chat-messages { + max-height: 180px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 8px; + } + + .helper__chat-msg { + font-size: 0.6875rem; + line-height: 1.4; + } + + .helper__chat-msg--user { + text-align: right; + } + + .helper__chat-role { + display: block; + font-size: 0.5625rem; + font-weight: 700; + color: var(--color-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 2px; + } + + .helper__chat-text { + color: var(--color-text-primary); + } + + .helper__chat-streaming { + max-height: 180px; + overflow-y: auto; + } + + .helper__chat-stage { + font-size: 0.5625rem; + color: var(--color-brand-primary); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 4px; + animation: stage-pulse 1.5s ease-in-out infinite; + } + + @keyframes stage-pulse { + 0%, 100% { opacity: 0.6; } + 50% { opacity: 1; } + } + + .helper__footer-sep { + color: var(--color-text-secondary); + font-size: 0.625rem; + opacity: 0.4; + } + + /* Footer */ + .helper__bubble-footer { + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 4px 16px 10px; + } + + .helper__footer-btn { + border: none; + background: transparent; + color: var(--color-text-secondary); + font-size: 0.625rem; + cursor: pointer; + text-decoration: underline; + text-decoration-style: dotted; + text-underline-offset: 2px; + + &:hover { + color: var(--color-text-primary); + } + } + + /* Bubble arrow */ + .helper__bubble-arrow { + position: absolute; + bottom: -7px; + right: 24px; + width: 14px; + height: 14px; + background: var(--color-surface-primary); + border-right: 1px solid var(--color-border-primary); + border-bottom: 1px solid var(--color-border-primary); + transform: rotate(45deg); + } + + /* ===== Send pulse (single burst when user hits Enter) ===== */ + .helper--send-pulse .helper__mascot { + animation: mascot-send-pulse 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; + } + + @keyframes mascot-send-pulse { + 0% { transform: scale(1); box-shadow: 0 4px 16px rgba(245,166,35,0.25); } + 40% { transform: scale(1.2); box-shadow: 0 0 30px rgba(245,166,35,0.6); } + 100% { transform: scale(1); box-shadow: 0 4px 16px rgba(245,166,35,0.25); } + } + + /* ===== Chat state: Thinking (head tilt + thought dots) ===== */ + .helper--chat-thinking .helper__mascot-img { + animation: mascot-thinking 1.5s ease-in-out infinite; + } + + @keyframes mascot-thinking { + 0%, 100% { transform: rotate(0deg); } + 25% { transform: rotate(-8deg) translateY(-2px); } + 75% { transform: rotate(5deg) translateY(-1px); } + } + + .helper__thought-dots { + position: absolute; + top: -14px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 3px; + } + + .helper__thought-dots span { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--color-brand-primary); + animation: thought-dot-bounce 1.2s ease-in-out infinite; + } + + .helper__thought-dots span:nth-child(2) { animation-delay: 0.2s; } + .helper__thought-dots span:nth-child(3) { animation-delay: 0.4s; } + + @keyframes thought-dot-bounce { + 0%, 100% { opacity: 0.3; transform: translateY(0); } + 50% { opacity: 1; transform: translateY(-7px); } + } + + /* ===== Chat state: Typing (gentle nod) ===== */ + .helper--chat-typing .helper__mascot-img { + animation: mascot-typing 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) infinite; + } + + @keyframes mascot-typing { + 0%, 100% { transform: translateY(0) scale(1); } + 30% { transform: translateY(-3px) scale(1.03); } + 60% { transform: translateY(-1px) scale(1.01); } + } + + /* ===== Chat state: Done (satisfied settle) ===== */ + .helper--chat-done .helper__mascot-img { + animation: mascot-done 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both; + } + + @keyframes mascot-done { + 0% { transform: scale(1); } + 40% { transform: scale(1.12) rotate(5deg); } + 70% { transform: scale(0.98) rotate(-2deg); } + 100% { transform: scale(1) rotate(0deg); } + } + + /* ===== Chat state: Error (head shake) ===== */ + .helper--chat-error .helper__mascot-img { + animation: mascot-error 0.5s ease-in-out; + } + + @keyframes mascot-error { + 0%, 100% { transform: translateX(0); } + 20% { transform: translateX(-4px) rotate(-3deg); } + 40% { transform: translateX(4px) rotate(3deg); } + 60% { transform: translateX(-3px) rotate(-2deg); } + 80% { transform: translateX(2px) rotate(1deg); } + } + + /* ===== Responsive ===== */ + @media (max-width: 575px) { + .helper { + bottom: 16px; + right: 16px; + } + + .helper__bubble { + width: 280px; + } + + .helper__mascot { + width: 48px; + height: 48px; + } + } + + /* ===== Respect reduced motion ===== */ + @media (prefers-reduced-motion: reduce) { + .helper--entering .helper__mascot, + .helper--idle-bounce .helper__mascot-img, + .helper--idle-wiggle .helper__mascot-img, + .helper--idle-peek .helper__mascot-img, + .helper--chat-thinking .helper__mascot-img, + .helper--chat-typing .helper__mascot-img, + .helper--chat-done .helper__mascot-img, + .helper--chat-error .helper__mascot-img, + .helper__thought-dots span, + .helper__search-zone, + .helper__bubble, + .helper__mascot-pulse { + animation: none !important; + } + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StellaHelperComponent implements OnInit, OnDestroy { + private readonly router = inject(Router); + readonly helperCtx = inject(StellaHelperContextService); + readonly assistant = inject(StellaAssistantService); + readonly assistantDrawer = inject(SearchAssistantDrawerService); + private routerSub?: Subscription; + private idleTimer?: ReturnType; + private autoTipTimer?: ReturnType; + + // Input field value + readonly inputValue = signal(''); + readonly sendPulse = signal(false); + readonly chatStage = signal(''); + + // Search results paging (3 cards per page) + private readonly SEARCH_PAGE_SIZE = 3; + readonly searchPage = signal(0); + readonly totalSearchPages = computed(() => + Math.ceil(this.assistant.searchResults().length / this.SEARCH_PAGE_SIZE) + ); + /** Which panel is active in the bubble: tips (default) or search (when typing) */ + readonly visiblePanel = computed<'tips' | 'search'>(() => { + const hasInput = this.inputValue().trim().length > 0; + const hasResults = this.assistant.searchResults().length > 0; + const isLoading = this.assistant.searchLoading(); + return (hasInput && (hasResults || isLoading)) ? 'search' : 'tips'; + }); + + readonly visibleSearchCards = computed(() => { + const all = this.assistant.searchResults(); + const start = this.searchPage() * this.SEARCH_PAGE_SIZE; + return all.slice(start, start + this.SEARCH_PAGE_SIZE); + }); + + // ---- State ---- + readonly prefs = signal(this.loadPrefs()); + readonly forceShow = signal(false); + readonly bubbleOpen = signal(false); + readonly entering = signal(true); + readonly idleState = signal<'none' | 'bounce' | 'wiggle' | 'peek'>('none'); + readonly currentPageKey = signal('dashboard'); + readonly currentTipIndex = signal(0); + + // ---- Derived ---- + readonly pageConfig = computed(() => { + const key = this.currentPageKey(); + return PAGE_TIPS[key] ?? PAGE_TIPS['default']; + }); + + readonly currentGreeting = computed(() => this.pageConfig().greeting); + + /** + * Effective tips: page tips + context-triggered tips from other pages. + * Context-triggered tips are prepended (higher priority). + */ + readonly effectiveTips = computed(() => { + const pageTips = this.pageConfig().tips; + const contexts = this.helperCtx.activeContexts(); + if (contexts.length === 0) return pageTips; + + // Find context-triggered tips across ALL page configs + const contextTips: StellaHelperTip[] = []; + for (const config of Object.values(PAGE_TIPS)) { + for (const tip of config.tips) { + if (tip.contextTrigger && contexts.includes(tip.contextTrigger)) { + // Avoid duplicates + if (!contextTips.some(t => t.title === tip.title)) { + contextTips.push(tip); + } + } + } + } + + // Context tips first, then page tips (deduped) + const combined = [...contextTips]; + for (const t of pageTips) { + if (!combined.some(c => c.title === t.title)) { + combined.push(t); + } + } + return combined; + }); + + readonly totalTips = computed(() => this.effectiveTips().length); + + readonly currentTip = computed(() => { + const tips = this.effectiveTips(); + const idx = this.currentTipIndex(); + return tips[idx] ?? null; + }); + + readonly isFirstVisit = computed(() => { + const key = this.currentPageKey(); + return !this.prefs().seenPages.includes(key); + }); + + constructor() { + // Persist prefs on change + effect(() => this.savePrefs(this.prefs())); + + // Sync service's isOpen → component's bubbleOpen + // When external callers (global search, Ctrl+K) set assistant.isOpen(true), + // the bubble should open. Uses allowSignalWrites to update bubbleOpen. + effect(() => { + const serviceOpen = this.assistant.isOpen(); + if (serviceOpen) { + this.bubbleOpen.set(true); + this.forceShow.set(true); // Override dismiss state for external callers + } + }, { allowSignalWrites: true }); + } + + ngOnInit(): void { + // Initialize unified assistant service + this.assistant.init(); + + // Set initial page + this.onPageChange(this.router.url); + + // Listen for route changes + this.routerSub = this.router.events + .pipe(filter((e): e is NavigationEnd => e instanceof NavigationEnd)) + .subscribe((e) => this.onPageChange(e.urlAfterRedirects ?? e.url)); + + // Entrance animation + setTimeout(() => this.entering.set(false), 800); + + // Auto-open bubble on first ever visit + if (this.prefs().seenPages.length === 0) { + setTimeout(() => this.bubbleOpen.set(true), 1200); + } + + // Idle animation cycle + this.idleTimer = setInterval(() => { + if (!this.bubbleOpen()) { + const anims: Array<'bounce' | 'wiggle' | 'peek'> = ['bounce', 'wiggle', 'peek']; + const pick = anims[Math.floor(Math.random() * anims.length)]; + this.idleState.set(pick); + setTimeout(() => this.idleState.set('none'), 1000); + } + }, IDLE_ANIMATION_INTERVAL); + } + + ngOnDestroy(): void { + this.routerSub?.unsubscribe(); + if (this.idleTimer) clearInterval(this.idleTimer); + if (this.autoTipTimer) clearInterval(this.autoTipTimer); + } + + // ---- Event handlers ---- + + onMascotClick(): void { + // If an active conversation exists and bubble is closed, reopen the drawer + if (!this.bubbleOpen() && this.assistant.activeConversationId()) { + this.assistant.openChat(); // resume existing conversation + return; + } + const open = !this.bubbleOpen(); + this.bubbleOpen.set(open); + if (open) { + this.assistant.isOpen.set(true); + this.markPageSeen(); + } + } + + onCloseBubble(): void { + this.bubbleOpen.set(false); + this.assistant.close(); + } + + onNextTip(): void { + const max = this.totalTips() - 1; + if (this.currentTipIndex() < max) { + this.currentTipIndex.update((i) => i + 1); + this.saveTipIndex(); + } + } + + onPrevTip(): void { + if (this.currentTipIndex() > 0) { + this.currentTipIndex.update((i) => i - 1); + this.saveTipIndex(); + } + } + + onAction(route: string): void { + this.bubbleOpen.set(false); + this.router.navigateByUrl(route); + } + + onDismiss(): void { + this.bubbleOpen.set(false); + this.assistant.dismiss(); + this.prefs.update((p) => ({ ...p, dismissed: true })); + this.forceShow.set(false); + } + + onRestore(): void { + this.prefs.update((p) => ({ ...p, dismissed: false })); + this.forceShow.set(true); + this.entering.set(true); + setTimeout(() => { + this.entering.set(false); + this.bubbleOpen.set(true); + }, 600); + } + + // ---- Unified mode handlers ---- + + onInputChange(event: Event): void { + const value = (event.target as HTMLInputElement).value; + this.inputValue.set(value); + // Live search on every keystroke (debounced 200ms in service) + this.searchPage.set(0); // reset to first page on new query + if (value.trim()) { + this.assistant.searchAsYouType(value); + } else { + this.assistant.clearSearch(); + } + } + + onInputFocus(): void { + // Input focus doesn't change mode — the mascot stays in tips mode. + // Submitting decides: keyword → global search bar, question → chat drawer. + } + + onInputSubmit(): void { + const value = this.inputValue().trim(); + if (!value) return; + + // Trigger a single pulse on the mascot before opening chat + this.sendPulse.set(true); + setTimeout(() => this.sendPulse.set(false), 600); + + // Enter always opens the AI chat drawer with context from search results + this.assistant.openChat(value); + this.inputValue.set(''); + this.assistant.clearSearch(); + } + + onSearchCardClick(card: any): void { + // Find a navigate action on the card + const navAction = card.actions?.find((a: any) => a.actionType === 'navigate' && a.route); + if (navAction?.route) { + this.bubbleOpen.set(false); + this.router.navigateByUrl(navAction.route); + } + } + + onPrevSearchPage(): void { + if (this.searchPage() > 0) this.searchPage.update(p => p - 1); + } + + onNextSearchPage(): void { + if (this.searchPage() < this.totalSearchPages() - 1) this.searchPage.update(p => p + 1); + } + + onAskStella(): void { + this.assistant.openChat(); + } + + onBackToTips(): void { + this.assistant.enterTips(); + this.inputValue.set(''); + } + + // ---- Internal ---- + + private onPageChange(url: string): void { + const key = resolvePageKey(url); + if (key === this.currentPageKey()) return; + + // Clear context signals from the previous page + this.helperCtx.clear(); + + this.currentPageKey.set(key); + + // Restore saved tip index for this page, or reset to 0 + const saved = this.prefs().tipIndex[key]; + this.currentTipIndex.set(saved ?? 0); + + // On first visit to a page, auto-open with greeting + if (this.isFirstVisit() && !this.prefs().dismissed) { + setTimeout(() => { + this.bubbleOpen.set(true); + this.markPageSeen(); + }, 800); + } + } + + private markPageSeen(): void { + const key = this.currentPageKey(); + this.prefs.update((p) => { + if (p.seenPages.includes(key)) return p; + return { ...p, seenPages: [...p.seenPages, key] }; + }); + } + + private saveTipIndex(): void { + const key = this.currentPageKey(); + const idx = this.currentTipIndex(); + this.prefs.update((p) => ({ + ...p, + tipIndex: { ...p.tipIndex, [key]: idx }, + })); + } + + private loadPrefs(): HelperPreferences { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + return { + dismissed: typeof parsed.dismissed === 'boolean' ? parsed.dismissed : DEFAULTS.dismissed, + seenPages: Array.isArray(parsed.seenPages) ? parsed.seenPages : DEFAULTS.seenPages, + tipIndex: parsed.tipIndex && typeof parsed.tipIndex === 'object' ? parsed.tipIndex : DEFAULTS.tipIndex, + }; + } + } catch { /* ignore */ } + return { ...DEFAULTS }; + } + + private savePrefs(prefs: HelperPreferences): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs)); + } catch { /* ignore quota / private-mode */ } + } +} diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-tour.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-tour.component.ts new file mode 100644 index 000000000..047e262ba --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-helper/stella-tour.component.ts @@ -0,0 +1,466 @@ +import { + Component, + ChangeDetectionStrategy, + inject, + signal, + computed, + OnDestroy, +} from '@angular/core'; +import { Router } from '@angular/router'; +import { StellaAssistantService } from './stella-assistant.service'; + +/** + * Tour step definition (from DB or static). + */ +export interface TourStep { + stepOrder: number; + route: string; + selector?: string; + title: string; + body: string; + action?: { label: string; route: string }; +} + +export interface Tour { + tourKey: string; + title: string; + description: string; + steps: TourStep[]; +} + +/** + * StellaTourComponent — Guided walkthrough engine. + * + * Shows the Stella mascot at each step, highlighting the target element + * with a backdrop overlay. Steps can navigate to different pages. + * + * Usage: + * + */ +@Component({ + selector: 'app-stella-tour', + standalone: true, + imports: [], + template: ` + @if (isActive()) { + +
+ @if (highlightRect(); as rect) { +
+ } +
+ + + + } + `, + styles: [` + .tour-backdrop { + position: fixed; + inset: 0; + z-index: 900; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(2px); + animation: tour-fade-in 0.3s ease; + } + + @keyframes tour-fade-in { + from { opacity: 0; } + to { opacity: 1; } + } + + .tour-highlight { + position: absolute; + border: 2px solid var(--color-brand-primary); + border-radius: var(--radius-md, 8px); + box-shadow: + 0 0 0 9999px rgba(0, 0, 0, 0.55), + 0 0 24px rgba(245, 166, 35, 0.4); + pointer-events: none; + transition: all 0.3s ease; + } + + .tour-card { + position: fixed; + z-index: 950; + width: 340px; + max-width: calc(100vw - 32px); + background: var(--color-surface-primary); + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-xl, 12px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2); + animation: tour-card-enter 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + overflow: hidden; + } + + @keyframes tour-card-enter { + from { opacity: 0; transform: translateY(16px) scale(0.95); } + to { opacity: 1; transform: translateY(0) scale(1); } + } + + .tour-card__header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 12px 8px; + } + + .tour-card__mascot { + border-radius: 50%; + border: 2px solid var(--color-brand-primary); + flex-shrink: 0; + } + + .tour-card__meta { + flex: 1; + display: flex; + flex-direction: column; + gap: 1px; + } + + .tour-card__tour-title { + font-size: 0.625rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-brand-primary); + } + + .tour-card__step-count { + font-size: 0.625rem; + color: var(--color-text-secondary); + } + + .tour-card__close { + width: 28px; + height: 28px; + border: none; + border-radius: 50%; + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + background: var(--color-surface-tertiary); + color: var(--color-text-primary); + } + } + + .tour-card__body { + padding: 0 16px 12px; + } + + .tour-card__title { + font-size: 0.8125rem; + font-weight: 700; + color: var(--color-text-heading); + margin-bottom: 4px; + } + + .tour-card__text { + font-size: 0.75rem; + line-height: 1.55; + color: var(--color-text-primary); + } + + .tour-card__action { + display: inline-flex; + align-items: center; + gap: 4px; + margin-top: 8px; + padding: 4px 10px; + border: 1px solid var(--color-brand-primary); + border-radius: var(--radius-md, 8px); + background: transparent; + color: var(--color-brand-primary); + font-size: 0.6875rem; + font-weight: 600; + cursor: pointer; + + &:hover { + background: var(--color-brand-primary); + color: var(--color-text-heading); + } + } + + .tour-card__progress { + height: 3px; + background: var(--color-surface-tertiary); + } + + .tour-card__progress-fill { + height: 100%; + background: var(--color-brand-primary); + transition: width 0.3s ease; + } + + .tour-card__nav { + display: flex; + justify-content: space-between; + padding: 8px 12px; + } + + .tour-card__nav-btn { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 5px 12px; + border: 1px solid var(--color-border-primary); + border-radius: var(--radius-md, 8px); + background: transparent; + color: var(--color-text-primary); + font-size: 0.6875rem; + font-weight: 600; + cursor: pointer; + + &:disabled { + opacity: 0.3; + cursor: default; + } + + &:hover:not(:disabled) { + border-color: var(--color-brand-primary); + } + } + + .tour-card__nav-btn--primary { + background: var(--color-brand-primary); + border-color: var(--color-brand-primary); + color: var(--color-text-heading); + + &:hover:not(:disabled) { + opacity: 0.9; + } + } + + @media (prefers-reduced-motion: reduce) { + .tour-backdrop, .tour-card { animation: none; } + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StellaTourComponent implements OnDestroy { + private readonly router = inject(Router); + private readonly assistant = inject(StellaAssistantService); + + // ---- State ---- + readonly activeTour = signal(null); + readonly currentStepIndex = signal(0); + private resizeObserver?: ResizeObserver; + + // ---- Derived ---- + readonly isActive = computed(() => this.activeTour() !== null); + readonly tourTitle = computed(() => this.activeTour()?.title ?? ''); + readonly totalSteps = computed(() => this.activeTour()?.steps.length ?? 0); + readonly currentStep = computed(() => { + const tour = this.activeTour(); + if (!tour) return null; + return tour.steps[this.currentStepIndex()] ?? null; + }); + readonly progressPercent = computed(() => { + const total = this.totalSteps(); + return total > 0 ? ((this.currentStepIndex() + 1) / total) * 100 : 0; + }); + + readonly highlightRect = signal(null); + readonly cardPosition = computed(() => { + const rect = this.highlightRect(); + if (!rect) { + // Center the card + return { top: window.innerHeight / 2 - 120, left: window.innerWidth / 2 - 170 }; + } + // Position card below or beside the highlighted element + const cardWidth = 340; + const cardHeight = 240; + let top = rect.bottom + 16; + let left = rect.left; + + // If card would go below viewport, place it above + if (top + cardHeight > window.innerHeight - 16) { + top = rect.top - cardHeight - 16; + } + // Keep within horizontal bounds + if (left + cardWidth > window.innerWidth - 16) { + left = window.innerWidth - cardWidth - 16; + } + if (left < 16) left = 16; + if (top < 16) top = 16; + + return { top, left }; + }); + + ngOnDestroy(): void { + this.resizeObserver?.disconnect(); + } + + // ---- Public API ---- + + startTour(tour: Tour): void { + this.activeTour.set(tour); + this.currentStepIndex.set(0); + this.navigateToStep(0); + } + + // ---- Handlers ---- + + onNext(): void { + const nextIdx = this.currentStepIndex() + 1; + if (nextIdx < this.totalSteps()) { + this.currentStepIndex.set(nextIdx); + this.navigateToStep(nextIdx); + } + } + + onPrev(): void { + const prevIdx = this.currentStepIndex() - 1; + if (prevIdx >= 0) { + this.currentStepIndex.set(prevIdx); + this.navigateToStep(prevIdx); + } + } + + onSkip(): void { + this.endTour(false); + } + + onFinish(): void { + this.endTour(true); + } + + onStepAction(route: string): void { + this.router.navigateByUrl(route); + } + + // ---- Internal ---- + + private navigateToStep(idx: number): void { + const step = this.activeTour()?.steps[idx]; + if (!step) return; + + // Navigate to the step's route if different from current + const currentPath = window.location.pathname; + if (step.route && step.route !== currentPath) { + this.router.navigateByUrl(step.route).then(() => { + setTimeout(() => this.highlightElement(step.selector), 500); + }); + } else { + this.highlightElement(step.selector); + } + } + + private highlightElement(selector?: string): void { + if (!selector) { + this.highlightRect.set(null); + return; + } + + const el = document.querySelector(selector); + if (el) { + const rect = el.getBoundingClientRect(); + this.highlightRect.set(rect); + + // Scroll element into view if needed + el.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } else { + this.highlightRect.set(null); + } + } + + private endTour(completed: boolean): void { + const tour = this.activeTour(); + if (tour && completed) { + this.assistant.userState.update(s => ({ + ...s, + completedTours: s.completedTours.includes(tour.tourKey) + ? s.completedTours + : [...s.completedTours, tour.tourKey], + })); + } + this.activeTour.set(null); + this.highlightRect.set(null); + } +} diff --git a/src/Web/StellaOps.Web/src/app/shared/components/stella-quick-links/stella-quick-links.component.ts b/src/Web/StellaOps.Web/src/app/shared/components/stella-quick-links/stella-quick-links.component.ts index 8b0ccc526..7709dfa83 100644 --- a/src/Web/StellaOps.Web/src/app/shared/components/stella-quick-links/stella-quick-links.component.ts +++ b/src/Web/StellaOps.Web/src/app/shared/components/stella-quick-links/stella-quick-links.component.ts @@ -4,14 +4,27 @@ * Gentle, unobtrusive navigational quick-links rendered as subtle inline text * with arrow indicators. Replaces all pill/card-based quick-link patterns. * + * Layouts: + * - 'inline' — horizontal dots (minimal, for contextual links) + * - 'aside' — vertical list with descriptions (sidebar/bottom positions) + * - 'strip' — horizontal header strip with title + description, right-fade + * gradient and scroll button when overflowing (canonical header pattern) + * * Usage: - * + * */ import { + AfterViewInit, ChangeDetectionStrategy, Component, + ElementRef, Input, + NgZone, + OnDestroy, + ViewChild, + inject, + signal, } from '@angular/core'; import { RouterLink } from '@angular/router'; @@ -25,7 +38,7 @@ export interface StellaQuickLink { icon?: string; /** Tooltip text */ hint?: string; - /** Short description shown below the label in aside layout */ + /** Short description shown below the label in aside/strip layout */ description?: string; } @@ -35,10 +48,56 @@ export interface StellaQuickLink { imports: [RouterLink], changeDetection: ChangeDetectionStrategy.OnPush, template: ` - @if (label) { + @if (label && layout !== 'strip') { {{ label }} } - @if (layout === 'aside') { + @if (layout === 'strip') { +
+ @if (label) { + {{ label }}: + } +
+ @if (canScrollLeft()) { +
+ + } + + @if (canScrollRight()) { +
+ + } +
+
+ } @else if (layout === 'aside') {