diff --git a/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java b/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java index 272917ec7f..b2be7b668e 100644 --- a/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java +++ b/app/proprietary/src/main/java/stirling/software/proprietary/policy/overview/PolicyOverviewService.java @@ -20,23 +20,28 @@ import stirling.software.proprietary.policy.source.SourceStore; import stirling.software.proprietary.policy.store.PolicyStore; /** - * Builds the Pipelines overview: every policy the caller's team owns, each annotated with its - * referenced sources (resolved to display names), its pipeline steps, and a trigger/output summary. - * Source names are resolved from the team's sources in memory rather than persisted on the policy, - * so the view always reflects the live source set. This is the "all pipelines" admin surface; the - * user-facing Policies page builds only a friendly subset of the same backend policies. + * Builds the Pipelines overview: one row per policy the caller's team built on the Pipelines page, + * with its sources resolved to live display names, its steps, and a trigger/output summary. + * Frontend/catalogue policies (marked by a {@code categoryId} in their output options) belong to + * the user-facing Policies page and are excluded; a folder-watch trigger is not a signal. */ @Service @RequiredArgsConstructor public class PolicyOverviewService { + // Output-options key marking a frontend/catalogue policy (set by the Policies page and seeder). + private static final String CATEGORY_OPTION = "categoryId"; + private final PolicyStore policyStore; private final SourceStore sourceStore; private final PolicyAccessGuard policyAccessGuard; private final SourceAccessGuard sourceAccessGuard; public PoliciesOverviewResponse overview() { - List policies = policyAccessGuard.visibleFrom(policyStore); + List policies = + policyAccessGuard.visibleFrom(policyStore).stream() + .filter(PolicyOverviewService::isPipeline) + .toList(); Map sourceNames = sourceNames(); List views = @@ -50,6 +55,18 @@ public class PolicyOverviewService { return new PoliciesOverviewResponse(buildKpis(policies), views); } + private static boolean isPipeline(Policy policy) { + return !isCataloguePolicy(policy); + } + + /** A frontend/catalogue policy, marked by a {@code categoryId} in its output options. */ + private static boolean isCataloguePolicy(Policy policy) { + OutputSpec output = policy.output(); + return output != null + && output.options().get(CATEGORY_OPTION) instanceof String category + && !category.isBlank(); + } + /** Display names for every source the caller's team can see, keyed by source id. */ private Map sourceNames() { Map names = new HashMap<>(); diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java index 8610926c2a..373b596136 100644 --- a/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java +++ b/app/proprietary/src/test/java/stirling/software/proprietary/policy/overview/PolicyOverviewServiceTest.java @@ -28,9 +28,11 @@ import stirling.software.proprietary.policy.store.InProcessPolicyStore; import stirling.software.proprietary.policy.store.PolicyStore; /** - * Tests for {@link PolicyOverviewService}: every policy appears once with its sources resolved to - * names, its steps and trigger/output summarised, and the KPI strip counting active vs paused. - * Login is disabled so the team guards pass everything through. + * Tests for {@link PolicyOverviewService}: every Pipelines-page policy appears once with its + * sources resolved to names, its steps and trigger/output summarised, and the KPI strip counting + * active vs paused. Frontend/catalogue policies (owned by the Policies page) are excluded, while a + * pipeline that uses a folder-watch trigger stays. Login is disabled so the team guards pass + * everything through. */ class PolicyOverviewServiceTest { @@ -95,6 +97,51 @@ class PolicyOverviewServiceTest { assertEquals(List.of(2L, 1L, 1L), response.kpis().stream().map(PolicyKpi::value).toList()); } + @Test + void excludesCataloguePoliciesButKeepsFolderWatchPipelines() { + Source inbox = source("Inbox", "/inbox"); + // A hand-built pipeline: shows. + policyStore.save( + new Policy( + null, + "Compress pipeline", + "owner", + true, + List.of(), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.inline())); + // A folder-watch pipeline is still a pipeline: shows. + policyStore.save( + new Policy( + null, + "Inbox watcher", + "owner", + true, + List.of( + new PipelineInput( + inbox.id(), new TriggerConfig("folder-watch", Map.of()))), + List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())), + OutputSpec.inline())); + // A frontend/catalogue policy (categoryId in output options): hidden. + policyStore.save( + new Policy( + null, + "Classification Policy", + "system", + true, + List.of(), + List.of(new PipelineStep("/api/v1/ai/tools/classify-and-label", Map.of())), + new OutputSpec("inline", Map.of("categoryId", "classification")))); + + PoliciesOverviewResponse response = service.overview(); + + assertEquals( + List.of("Compress pipeline", "Inbox watcher"), + response.pipelines().stream().map(PolicyView::name).toList()); + // KPIs count both visible pipelines, not the hidden catalogue policy. + assertEquals(List.of(2L, 2L, 0L), response.kpis().stream().map(PolicyKpi::value).toList()); + } + @Test void anUnresolvedSourceFallsBackToItsId() { policyStore.save( diff --git a/frontend/editor/src/portal/mocks/handlers/pipelines.ts b/frontend/editor/src/portal/mocks/handlers/pipelines.ts index 0158d1e168..ed7cec1626 100644 --- a/frontend/editor/src/portal/mocks/handlers/pipelines.ts +++ b/frontend/editor/src/portal/mocks/handlers/pipelines.ts @@ -152,9 +152,16 @@ function toView(policy: StoredPolicy): PipelineView { }; } -function buildKpis(): PipelineKpi[] { - const total = store.length; - const active = store.filter((p) => p.enabled).length; +// Mirrors the backend PolicyOverviewService: hide frontend/catalogue policies (a categoryId in +// output options). A folder-watch trigger is still a normal pipeline and stays. +function isPipeline(policy: StoredPolicy): boolean { + const categoryId = policy.output?.options?.categoryId; + return !(typeof categoryId === "string" && categoryId.length > 0); +} + +function buildKpis(policies: StoredPolicy[]): PipelineKpi[] { + const total = policies.length; + const active = policies.filter((p) => p.enabled).length; return [ { value: total, description: "pipelines" }, { value: active, description: "running automatically" }, @@ -163,10 +170,11 @@ function buildKpis(): PipelineKpi[] { } function buildOverview(): PipelinesOverviewResponse { - const pipelines = store + const visible = store.filter(isPipeline); + const pipelines = visible .map(toView) .sort((a, b) => a.name.localeCompare(b.name)); - return { kpis: buildKpis(), pipelines }; + return { kpis: buildKpis(visible), pipelines }; } export const pipelinesHandlers = [