diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json new file mode 100644 index 000000000..6473c2cd1 --- /dev/null +++ b/docs/release-control/v6/internal/status.json @@ -0,0 +1,3707 @@ +{ + "version": "6.0", + "updated_at": "2026-03-20", + "scope": { + "active_repos": [ + "pulse", + "pulse-enterprise", + "pulse-mobile", + "pulse-pro" + ], + "control_plane_repo": "pulse", + "ignored_repos": [ + "pulse-5.1.x", + "pulse-refactor-streams" + ], + "repo_catalog": [ + { + "id": "pulse", + "purpose": "Core desktop/runtime repo and canonical Pulse v6 release-control authority.", + "visibility": "public" + }, + { + "id": "pulse-enterprise", + "purpose": "Closed-source enterprise and paid runtime features that should not ship as open-source surfaces.", + "visibility": "private" + }, + { + "id": "pulse-mobile", + "purpose": "Mobile client, mobile relay flows, approval UX, and device-local auth/state handling.", + "visibility": "private" + }, + { + "id": "pulse-pro", + "purpose": "Financial, operational, checkout, license-server, and relay-server surfaces that back commercial flows.", + "visibility": "private" + } + ] + }, + "source_precedence": [ + "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", + "docs/release-control/v6/internal/status.json", + "docs/release-control/v6/status.schema.json", + "docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md", + "docs/release-control/v6/internal/subsystems/registry.json", + "docs/release-control/v6/internal/subsystems/registry.schema.json" + ], + "execution_model": "direct-repo-sessions", + "readiness": { + "repo_ready_rule": "all lanes target-met and evidence-present plus all repo-ready assertions passed", + "rc_ready_rule": "repo_ready plus all rc-ready assertions passed plus zero rc-ready open_decisions plus all rc-ready release_gates passed", + "release_ready_rule": "rc_ready plus all release-ready assertions passed plus zero release-ready open_decisions plus all release-ready release_gates passed" + }, + "readiness_assertions": [ + { + "id": "RA1", + "summary": "Governed surfaces that should use the unified resource model do not keep shipping on legacy equivalents.", + "kind": "invariant", + "blocking_level": "repo-ready", + "proof_type": "automated", + "lane_ids": [ + "L6", + "L13" + ], + "subsystem_ids": [ + "api-contracts", + "unified-resources" + ], + "release_gate_ids": [], + "proof_commands": [ + { + "id": "ra1-unified-resource-guardrails", + "run": [ + "go", + "test", + "./internal/unifiedresources", + "-run", + "TestNoDirectStateAccessForMigratedResources|TestNoLegacyHostResourceTypeSymbol|TestNoLegacyMigrationHintsInRuntimeCode|TestV6AgentRegistrationArtifactsStayCanonical|TestV6BroadLegacyAliasCoverage|TestV6ReleaseFacingAPITestsCoverLegacyHostRejection|TestV6DirectHostAliasValidatorCoverage", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.workloads-link.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/hooks/__tests__/useUnifiedResources.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/unifiedresources/code_standards_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA2", + "summary": "New users can complete Pulse Pro Relay signup through paid activation without manual operator intervention or ambiguous provisioning steps.", + "kind": "journey", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L2", + "L3", + "L8", + "L12" + ], + "subsystem_ids": [ + "cloud-paid", + "frontend-primitives" + ], + "release_gate_ids": [ + "hosted-signup-billing-replay" + ], + "proof_commands": [ + { + "id": "hosted-signup-api-tests", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestHostedLifecycle|TestHostedSignupSuccess|TestHostedSignupValidationFailures|TestHostedSignupHostedModeGate|TestHostedSignupRateLimit|TestHostedSignupRateLimit_NoProvisioningSideEffects|TestHostedSignupCleanupOnRBACFailure|TestHostedSignupFailsClosedWithoutPublicURL", + "-count=1" + ] + }, + { + "id": "hosted-signup-provisioner-tests", + "run": [ + "go", + "test", + "./internal/hosted", + "-run", + "TestProvisionTenantSuccess|TestProvisionTenantIdempotentDuplicateEmail|TestProvisionTenantIdempotentDuplicateEmailCaseInsensitive|TestProvisionTenantValidationFailures|TestProvisionTenantPartialFailureRollback|TestProvisionHostedSignupSuccess", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/hosted-signup-billing-replay-2026-03-12.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/hosted-signup-billing-replay-production-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/hosted-signup-billing-replay-production-fixed-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/hosted_lifecycle_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/hosted_signup_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/hosted/provisioner_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA3", + "summary": "After first successful entitlement activation, Pulse preserves paid state across supported sessions and upgrades without repeated license entry.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L3", + "L5", + "L8" + ], + "subsystem_ids": [ + "cloud-paid", + "frontend-primitives" + ], + "release_gate_ids": [ + "paid-feature-entitlement-gating", + "upgrade-state-and-entitlement-preservation" + ], + "proof_commands": [ + { + "id": "entitlement-gating-api-tests", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestHandleActivateLicense_ExchangesLegacyJWTInStrictV6|TestHandleActivateLicense_ClearsCommercialMigrationStateOnNativeActivation|TestHandleActivateLicense_ActivationKeyClearsStaleLegacyPersistence|TestGetTenantComponents_AutoExchangesPersistedLegacyJWT|TestGetTenantComponents_SkipsExchange_WhenActivationStateExists|TestGetTenantComponents_PersistsCommercialMigrationState_WhenAutoExchangeFails|TestRequireLicenseFeature_HostedEntitlementsBlockMissingFeature|TestRequireLicenseFeature_HostedEntitlementsAllowGrantedFeature|TestLicenseGatedEmptyResponse_HostedEntitlementsReturnEmptyArrayWhenLocked", + "-count=1" + ] + }, + { + "id": "upgrade-state-migration-tests", + "run": [ + "go", + "test", + "./tests/migration", + "-run", + "TestV5PaidLicenseUpgrade_CommercialMigrationFailureMatrix|TestV5PaidLicenseUpgrade_RealLicenseServerExchange|TestV5DataDir_CSRFLegacyMapFormat|TestV5DataDir_CSRFTokenFileContinuity|TestV5DataDir_SessionLegacyMapFormat|TestV5DataDir_SessionTokenContinuity|TestV5DowngradeSafety|TestV5FullUpgradeScenario", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/upgrade-state-and-entitlement-preservation-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/license_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/licensing_handlers_auto_migrate_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_commercial_migration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_real_exchange_upgrade_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_session_db_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA4", + "summary": "Typical end users cannot trivially unlock Pulse Pro features by stripping client-only checks while server and hosted entitlements still deny access.", + "kind": "trust-gate", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L2", + "L3", + "L6", + "L9", + "L12" + ], + "subsystem_ids": [ + "api-contracts", + "cloud-paid" + ], + "release_gate_ids": [ + "paid-feature-entitlement-gating" + ], + "proof_commands": [ + { + "id": "entitlement-gating-api-tests", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestHandleActivateLicense_ExchangesLegacyJWTInStrictV6|TestHandleActivateLicense_ClearsCommercialMigrationStateOnNativeActivation|TestHandleActivateLicense_ActivationKeyClearsStaleLegacyPersistence|TestGetTenantComponents_AutoExchangesPersistedLegacyJWT|TestGetTenantComponents_SkipsExchange_WhenActivationStateExists|TestGetTenantComponents_PersistsCommercialMigrationState_WhenAutoExchangeFails|TestRequireLicenseFeature_HostedEntitlementsBlockMissingFeature|TestRequireLicenseFeature_HostedEntitlementsAllowGrantedFeature|TestLicenseGatedEmptyResponse_HostedEntitlementsReturnEmptyArrayWhenLocked", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/license_handlers_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA5", + "summary": "Non-paid users do not see paid-only navigation or pages unless the surface is explicitly promotional.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L2", + "L3", + "L8", + "L9", + "L12" + ], + "subsystem_ids": [ + "cloud-paid", + "frontend-primitives" + ], + "release_gate_ids": [ + "paid-feature-entitlement-gating" + ], + "proof_commands": [ + { + "id": "frontend-paid-surface-visibility-tests", + "cwd": "frontend-modern", + "run": [ + "npm", + "test", + "--", + "--run", + "src/components/Dashboard/__tests__/RelayOnboardingCard.test.tsx", + "src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "src/components/Settings/__tests__/settingsNavigation.integration.test.tsx", + "src/pages/__tests__/AIIntelligence.test.tsx" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "frontend-modern/src/components/Dashboard/__tests__/RelayOnboardingCard.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx", + "kind": "file" + } + ] + }, + { + "id": "RA6", + "summary": "Supported upgrades preserve core state, entitlements, and first-session continuity without manual repair steps.", + "kind": "journey", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L3", + "L5", + "L8" + ], + "subsystem_ids": [ + "cloud-paid", + "frontend-primitives" + ], + "release_gate_ids": [ + "upgrade-state-and-entitlement-preservation" + ], + "proof_commands": [ + { + "id": "csrf-state-migration-api-tests", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestCSRFTokenStore_Load_CurrentFormat_SkipsNilAndExpired|TestCSRFTokenStore_Load_MigratesLegacyFormat", + "-count=1" + ] + }, + { + "id": "upgrade-state-migration-tests", + "run": [ + "go", + "test", + "./tests/migration", + "-run", + "TestV5PaidLicenseUpgrade_CommercialMigrationFailureMatrix|TestV5PaidLicenseUpgrade_RealLicenseServerExchange|TestV5DataDir_CSRFLegacyMapFormat|TestV5DataDir_CSRFTokenFileContinuity|TestV5DataDir_SessionLegacyMapFormat|TestV5DataDir_SessionTokenContinuity|TestV5DowngradeSafety|TestV5FullUpgradeScenario", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/upgrade-state-and-entitlement-preservation-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/csrf_store_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/licensing_handlers_auto_migrate_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/11-first-session.spec.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_full_upgrade_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_real_exchange_upgrade_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_session_db_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA7", + "summary": "Monitored-system allocation stays coherent across enforcement, entitlements, and UI: counted top-level monitored systems are collection-path agnostic, capped limits stay canonical, and users see the same monitored-system usage that the runtime enforces.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L2", + "L3", + "L6", + "L8", + "L12", + "L16" + ], + "subsystem_ids": [ + "api-contracts", + "cloud-paid", + "frontend-primitives" + ], + "release_gate_ids": [ + "paid-feature-entitlement-gating" + ], + "proof_commands": [ + { + "id": "monitored-system-allocation-backend-tests", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestMonitoredSystemCountNilMonitor|TestLegacyConnectionCountsFromReadState|TestLegacyConnectionCountsUsesSnapshotFallback|TestDeployReservedCount|TestUnifiedAgentHandlers_HandleReport_EnforcesMaxMonitoredSystemsForNewHostsOnly|TestHandleAddNode_BlocksNewCountedSystemAtLimit|TestHandleAutoRegister_BlocksNewCountedSystemAtLimit|TestDockerAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit|TestKubernetesAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit|TestTrueNASHandlers_HandleAdd_BlocksNewCountedSystemAtLimit|TestBuildEntitlementPayloadWithUsage_CurrentValues", + "-count=1" + ] + }, + { + "cwd": "frontend-modern", + "id": "monitored-system-allocation-frontend-tests", + "run": [ + "npm", + "test", + "--", + "--run", + "src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx", + "src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx" + ] + }, + { + "id": "monitored-system-allocation-limit-contract-tests", + "run": [ + "go", + "test", + "./pkg/licensing", + "-run", + "TestExceedsMonitoredSystemLimit|TestInstalledUnifiedAgentCount|TestLimitsForCloudPlan_KnownPlans|TestLimitsForCloudPlan_UnknownPlanFailsClosed", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/config_handlers_add_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/config_handlers_auto_register_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/docker_agents_additional_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/entitlement_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/kubernetes_agents_additional_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/monitored_system_limit_enforcement_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/truenas_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/unified_agent_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "pkg/licensing/features_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "pkg/licensing/monitored_system_limit_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA8", + "summary": "Stable or GA promotion happens only after an exercised RC, live release-pipeline proof, a recorded rollback target, and a written v5 maintenance-only support policy.", + "kind": "trust-gate", + "blocking_level": "release-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L1", + "L9", + "L11", + "L12" + ], + "subsystem_ids": [], + "release_gate_ids": [ + "rc-to-ga-promotion-readiness" + ], + "proof_commands": [ + { + "id": "release-promotion-policy-tests", + "run": [ + "python3", + "scripts/release_control/release_promotion_policy_test.py" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": ".github/workflows/release-dry-run.yml", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/RC_TO_GA_REHEARSAL_TEMPLATE.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/RELEASE_PROMOTION_POLICY.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/V5_MAINTENANCE_SUPPORT_POLICY.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "scripts/release_control/release_promotion_policy_test.py", + "kind": "file" + } + ] + }, + { + "id": "RA9", + "summary": "A real v5-installed Pulse Unified Agent upgrades through v6 release assets into one canonical v6 agent identity, preserves one-shot update continuity metadata, keeps legacy persisted host-agent token scopes valid at the v6 canonical agent endpoints, and does not drift agent-count or fallback behavior during the crossover.", + "kind": "journey", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L11", + "L12", + "L16" + ], + "subsystem_ids": [ + "agent-lifecycle" + ], + "release_gate_ids": [ + "unified-agent-v5-upgrade-continuity" + ], + "proof_commands": [ + { + "id": "unified-agent-first-report-continuity-tests", + "run": [ + "go", + "test", + "./internal/hostagent", + "-run", + "TestNew_CarriesUpdatedFromIntoFirstV6Report|TestAgentSendReport_SetsHeadersAndPostsJSON", + "-count=1" + ] + }, + { + "id": "unified-agent-install-fallback-tests", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestDownloadUnifiedInstallScript|TestDownloadUnifiedInstallScriptPS|TestProxyInstallScriptFromGitHub|TestContract_InstallScriptReleaseAssetURL|TestDownloadUnifiedAgent|TestUnifiedAgentHandlers_LegacyV5ReportUpgradesToSingleCanonicalUnifiedAgent|TestUnifiedAgentEndpointsAcceptLegacyUnifiedAgentReportScopeAlias|TestNormalizeRequestedScopesCanonicalizesLegacyUnifiedAgentAliases|TestContract_APITokenScopeAliasNormalization", + "-count=1" + ] + }, + { + "id": "unified-agent-update-handoff-tests", + "run": [ + "go", + "test", + "./internal/agentupdate", + "-run", + "TestCheckAndUpdateToFirstHostReportCarriesPreviousVersionOnce|TestUpdateToFirstHostReportCarriesPreviousVersionOnce|TestPerformUpdatePersistsPreviousVersionForNextStart", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/unified-agent-v5-upgrade-continuity-2026-03-12.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/agentupdate/coverage_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/agentupdate/update_hostagent_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_regression_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_tokens_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/unified_agent_more_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/unified_agent_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/unified_agent_upgrade_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/config/api_tokens_coverage_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/hostagent/agent_new_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA10", + "summary": "Active Pulse v6 guidance reflects the current governed behavior of the codebase, and legacy or historical documentation does not remain part of the active v6 guidance surface.", + "kind": "invariant", + "blocking_level": "release-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L9" + ], + "subsystem_ids": [], + "release_gate_ids": [ + "documentation-currentness-and-legacy-cleanup" + ], + "proof_commands": [ + { + "id": "documentation-currentness-tests", + "run": [ + "python3", + "scripts/release_control/documentation_currentness_test.py" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/control_plane.json", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/internal/CONTROL_PLANE.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/documentation-currentness-and-legacy-cleanup-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/status.json", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/README.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "scripts/release_control/documentation_currentness_test.py", + "kind": "file" + } + ] + }, + { + "id": "RA11", + "summary": "Pulse Hosted works as a real offered v6 tier: customers can sign up or sign in to a hosted tenant, enter a working hosted runtime, and use hosted billing/admin surfaces without self-hosted fallback paths or broken post-provisioning behavior.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L3", + "L4", + "L12" + ], + "subsystem_ids": [ + "cloud-paid" + ], + "release_gate_ids": [ + "cloud-hosted-tier-runtime-readiness" + ], + "proof_commands": [ + { + "id": "cloud-hosted-tier-runtime-api", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestHostedLifecycle|TestHostedOrgAdminHandlers|TestHostedSignupSuccess|TestHostedSignupValidationFailures|TestHostedSignupHostedModeGate|TestHostedSignupRateLimit|TestHostedSignupRateLimit_NoProvisioningSideEffects|TestHostedSignupCleanupOnRBACFailure|TestHostedSignupFailsClosedWithoutPublicURL|TestStripeWebhook_", + "-count=1" + ] + }, + { + "id": "cloud-hosted-tier-runtime-frontend", + "run": [ + "npx", + "vitest", + "run", + "src/pages/__tests__/HostedSignup.test.tsx", + "src/components/Settings/__tests__/BillingAdminPanel.test.tsx", + "src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx" + ], + "cwd": "frontend-modern" + }, + { + "id": "cloud-hosted-tier-runtime-go", + "run": [ + "go", + "test", + "./internal/cloudcp/...", + "./internal/hosted/...", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-fixed-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-followup-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/cloud-paid.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/pages/__tests__/HostedSignup.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/hosted_lifecycle_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/hosted_org_admin_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/hosted_signup_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/cloudcp/stripe/cloud_lifecycle_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/hosted/provisioner_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA12", + "summary": "Multi-tenant Pulse is sound: tenant isolation, organization membership, RBAC scope, tenant-scoped runtime state, sharing, and migration all fail closed outside the intended tenant boundary instead of partially behaving like a single-tenant system.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L4", + "L6", + "L12", + "L13", + "L14" + ], + "subsystem_ids": [ + "api-contracts", + "monitoring", + "organization-settings" + ], + "release_gate_ids": [ + "multi-tenant-runtime-isolation-and-coherence" + ], + "proof_commands": [ + { + "id": "multi-tenant-runtime-api", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestOrgHandlers|TestMultiTenant|TestResourceHandlers_NonDefaultOrg|TestSetMultiTenantMonitor_WiresHandlers|TestMultiTenantStateProvider|TestMultiTenantAPITokenRemainsScopedToIssuingOrg", + "-count=1" + ] + }, + { + "id": "multi-tenant-runtime-frontend", + "run": [ + "npx", + "vitest", + "run", + "src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", + "src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "src/utils/__tests__/rbacPermissions.test.ts", + "src/utils/__tests__/rbacPresentation.test.ts", + "src/utils/__tests__/organizationRolePresentation.test.ts", + "src/utils/__tests__/organizationSettingsPresentation.test.ts" + ], + "cwd": "frontend-modern" + }, + { + "id": "multi-tenant-runtime-migration", + "run": [ + "go", + "test", + "./tests/migration", + "-run", + "TestV5DataDir_MultiTenantMigration", + "-count=1" + ] + }, + { + "id": "multi-tenant-runtime-monitoring", + "run": [ + "go", + "test", + "./internal/monitoring", + "-run", + "TestMultiTenantMonitor", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/multi-tenant-runtime-isolation-and-coherence-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/organization-settings.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/organizationRolePresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/rbacPermissions.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/rbacPresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/api_token_org_scope_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/org_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/rbac_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/resources_tenant_security_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/router_helpers_more_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/monitoring/multi_tenant_monitor_additional_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/03-multi-tenant.spec.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_to_v6_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA13", + "summary": "MSP support works as a real v6 product mode: one provider account can onboard, view, and manage multiple client tenants from one control surface with canonical MSP plan handling and without cross-client leakage, scope confusion, or per-client operational drift.", + "kind": "journey", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L3", + "L4", + "L12" + ], + "subsystem_ids": [ + "cloud-paid" + ], + "release_gate_ids": [ + "msp-provider-tenant-management" + ], + "proof_commands": [ + { + "id": "msp-account-and-registry-tests", + "run": [ + "go", + "test", + "./internal/cloudcp/account", + "./internal/cloudcp/registry", + "-count=1" + ] + }, + { + "id": "msp-frontend-tests", + "run": [ + "npx", + "vitest", + "run", + "src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx", + "src/pages/__tests__/CloudPricing.test.tsx" + ], + "cwd": "frontend-modern" + }, + { + "id": "msp-lifecycle-tests", + "run": [ + "go", + "test", + "./internal/cloudcp/stripe", + "-run", + "TestMSPLifecycle_AccountToPortal", + "-count=1" + ] + }, + { + "id": "msp-plan-guardrails", + "run": [ + "go", + "test", + "./internal/cloudcp", + "./pkg/licensing", + "-run", + "TestPublicCloudSignupCheckoutMetadataRejectsMSPPlanForPublicSignup|TestMSPPlanAliasCanonicalizationContract", + "-count=1" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-production-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-production-fixed-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-production-followup-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/cloud-paid.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/pages/__tests__/CloudPricing.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/cloudcp/account/handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/cloudcp/public_cloud_signup_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/cloudcp/registry/registry_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/cloudcp/stripe/msp_lifecycle_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "pkg/licensing/grant_claims_contract_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA14", + "summary": "API tokens remain least-privilege and correctly scoped: every token stays bound to its intended user, org, and scope, cannot silently widen authority through legacy alias handling, and stops working when revoked.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L1", + "L6", + "L11", + "L14", + "L16" + ], + "subsystem_ids": [ + "agent-lifecycle", + "api-contracts", + "security-privacy" + ], + "release_gate_ids": [ + "api-token-scope-and-assignment" + ], + "proof_commands": [ + { + "id": "api-token-scope-backend", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "Test(APIToken|SecurityTokens|SystemSettings|MultiTenant)|TestNormalizeRequestedScopesCanonicalizesLegacyUnifiedAgentAliases|TestUnifiedAgentEndpointsRequireAgentReportScope|TestUnifiedAgentEndpointsAcceptLegacyUnifiedAgentReportScopeAlias|TestMultiTenantAPITokenRemainsScopedToIssuingOrg|TestContract_APITokenScopeAliasNormalization", + "-count=1" + ] + }, + { + "id": "api-token-scope-frontend", + "run": [ + "npx", + "vitest", + "run", + "src/components/Settings/__tests__/APITokenManager.test.tsx", + "src/utils/__tests__/apiClient.org.test.ts", + "src/utils/__tests__/apiTokenPresentation.test.ts" + ], + "cwd": "frontend-modern" + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/api-token-scope-and-assignment-2026-03-12.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/security-privacy.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/api/__tests__/security.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/APITokenManager.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/apiClient.org.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/apiTokenPresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/api_token_org_scope_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/contract_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_regression_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_status_additional_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_tokens_owner_binding_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_tokens_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/system_settings_telemetry_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA15", + "summary": "Assigned user privileges fail closed: a user cannot view, mutate, or destroy anything beyond the permissions granted by their effective organization membership and role, including cross-org and destructive paths.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L4", + "L6", + "L12", + "L14" + ], + "subsystem_ids": [ + "api-contracts", + "organization-settings" + ], + "release_gate_ids": [ + "organization-user-scope-and-rbac" + ], + "proof_commands": [ + { + "id": "user-privilege-backend", + "run": [ + "go", + "test", + "./internal/api", + "-run", + "TestOrgHandlersViewerCannotManageOrg|TestOrgHandlersTokenListAllowedButWriteForbidden|TestOrgHandlersCrossOrgIsolation|TestOrgHandlersShareIsolationAcrossOrganizations|TestMultiTenantRBACRoleUpdateChangesPermissions", + "-count=1" + ] + }, + { + "id": "user-privilege-frontend", + "run": [ + "npx", + "vitest", + "run", + "src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", + "src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "src/utils/__tests__/organizationRolePresentation.test.ts", + "src/utils/__tests__/organizationSettingsPresentation.test.ts", + "src/utils/__tests__/rbacPermissions.test.ts", + "src/utils/__tests__/rbacPresentation.test.ts" + ], + "cwd": "frontend-modern" + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/organization-user-scope-and-rbac-2026-03-12.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/organizationRolePresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/rbacPermissions.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/rbacPresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/org_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/rbac_handlers_test.go", + "kind": "file" + } + ] + }, + { + "id": "RA16", + "summary": "Commercial continuity fails closed across cancellation and re-entry: active grandfathered v5 recurring customers keep their legacy recurring price only while the subscription remains continuous, cancellation revokes paid access cleanly, and any later re-entry uses current public v6 pricing rather than reviving grandfathered terms.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L2", + "L3", + "L11", + "L12" + ], + "subsystem_ids": [ + "cloud-paid" + ], + "release_gate_ids": [ + "commercial-cancellation-reactivation" + ], + "proof_commands": [ + { + "id": "commercial-cancellation-reactivation-proof", + "run": [ + "python3", + "scripts/release_control/internal/commercial_cancellation_reactivation_proof.py" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/COMMERCIAL_CANCELLATION_REACTIVATION_E2E_TEST_PLAN.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/commercial-cancellation-reactivation-2026-03-12.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/commercial-cancellation-reactivation-external-e2e-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "pkg/licensing/billing_state_normalization_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "scripts/release_control/internal/commercial_cancellation_reactivation_proof.py", + "kind": "file" + } + ] + }, + { + "id": "RA17", + "summary": "Pulse Mobile access stays coherent against a real Pulse instance: pairing, secure persistence, relay reconnect, approval flows, and auth or revocation transitions recover cleanly and fail closed instead of leaving stale mobile access behind.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L5", + "L6", + "L7", + "L12" + ], + "subsystem_ids": [ + "api-contracts", + "relay-runtime" + ], + "release_gate_ids": [ + "mobile-relay-auth-approvals" + ], + "proof_commands": [ + { + "id": "mobile-relay-auth-approvals-proof", + "run": [ + "python3", + "scripts/release_control/internal/mobile_relay_auth_approvals_proof.py" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/mobile-relay-auth-approvals-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "scripts/release_control/internal/mobile_relay_auth_approvals_proof.py", + "kind": "file" + }, + { + "repo": "pulse-enterprise", + "path": "internal/aiautofix/handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse-mobile", + "path": "src/__tests__/mobileRelayAuthApprovals.rehearsal.test.ts", + "kind": "file" + }, + { + "repo": "pulse-mobile", + "path": "src/hooks/__tests__/useRelayLifecycle.test.ts", + "kind": "file" + }, + { + "repo": "pulse-mobile", + "path": "src/stores/__tests__/approvalStore.test.ts", + "kind": "file" + }, + { + "repo": "pulse-mobile", + "path": "src/stores/__tests__/authStore.test.ts", + "kind": "file" + } + ] + }, + { + "id": "RA18", + "summary": "Relay runtime remains resilient under registration and reconnect pressure: fresh registration, reconnect, stale-session recovery, and disconnect drain all recover predictably without stranding clients in resume loops, dead sessions, or lost inflight work.", + "kind": "invariant", + "blocking_level": "rc-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L5", + "L6", + "L7", + "L12" + ], + "subsystem_ids": [ + "api-contracts", + "relay-runtime" + ], + "release_gate_ids": [ + "relay-registration-reconnect-drain" + ], + "proof_commands": [ + { + "id": "relay-registration-reconnect-drain-proof", + "run": [ + "python3", + "scripts/release_control/internal/relay_registration_reconnect_drain_proof.py" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/relay-registration-reconnect-drain-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Dashboard/__tests__/RelayOnboardingCard.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/relay/client_managed_runtime_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "scripts/release_control/internal/relay_registration_reconnect_drain_proof.py", + "kind": "file" + }, + { + "repo": "pulse-mobile", + "path": "src/relay/__tests__/client-hardening.test.ts", + "kind": "file" + } + ] + }, + { + "id": "RA19", + "summary": "Comparable Pulse settings surfaces use the canonical settings page-shell contract: shared SettingsPanel framing, consistent header structure, and no ad hoc top-level layout chrome unless explicitly approved.", + "kind": "invariant", + "blocking_level": "release-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L8" + ], + "subsystem_ids": [ + "frontend-primitives" + ], + "release_gate_ids": [ + "settings-surface-layout-consistency" + ], + "proof_commands": [ + { + "id": "settings-architecture-guardrails", + "cwd": "frontend-modern", + "run": [ + "npx", + "vitest", + "run", + "src/components/Settings/__tests__/settingsArchitecture.test.ts" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/settings-surface-layout-consistency-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/status.json", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/frontend-primitives.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/15-settings-shell-consistency.spec.ts", + "kind": "file" + } + ] + } + ], + "evidence_reference_policy": { + "format": "repo-qualified-relative-paths", + "allowed_kinds": [ + "file", + "dir" + ], + "absolute_paths_forbidden": true, + "local_repo": "pulse" + }, + "priority_engine": { + "formula": "behind_score = (max(0,target-current)*4) + criticality + staleness + dependency + blocker_bonus", + "floor_rule": { + "release_critical_lanes": [ + "L1", + "L2", + "L3", + "L7", + "L8", + "L9", + "L10", + "L11", + "L12" + ], + "minimum_score": 6 + }, + "weights": { + "gap_multiplier": 4, + "criticality_range": "0-5", + "staleness_range": "0-3", + "dependency_range": "0-3", + "blocker_bonus": 8 + } + }, + "lanes": [ + { + "id": "L1", + "name": "Self-hosted release confidence", + "target_score": 9, + "current_score": 9, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Self-hosted RC confidence is at the current floor; GA promotion still depends on the shared RC-to-GA gate plus the lane-local self-hosted promotion package follow-up.", + "tracking": [ + { + "kind": "lane-followup", + "id": "self-hosted-ga-promotion-package" + }, + { + "kind": "release-gate", + "id": "rc-to-ga-promotion-readiness" + } + ] + }, + "blockers": [], + "subsystems": [ + "deployment-installability" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/deployment-installability.md", + "kind": "file" + } + ] + }, + { + "id": "L2", + "name": "Conversion/commercial readiness", + "target_score": 9, + "current_score": 9, + "status": "target-met", + "completion": { + "state": "complete", + "summary": "Commercial conversion surfaces are at the current tracked RC floor, including real cancellation/reactivation continuity through the governed external Stripe rehearsal.", + "tracking": [] + }, + "blockers": [], + "subsystems": [], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/COMMERCIAL_CANCELLATION_REACTIVATION_E2E_TEST_PLAN.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-msp-price-audit-2026-03-13.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/cloud-paid.md", + "kind": "file" + }, + { + "repo": "pulse-pro", + "path": "V6_LAUNCH_CHECKLIST.md", + "kind": "file" + } + ] + }, + { + "id": "L3", + "name": "Cloud paid readiness", + "target_score": 8, + "current_score": 8, + "status": "target-met", + "completion": { + "state": "complete", + "summary": "Cloud-paid runtime and billing surfaces are at the current tracked RC floor, including real cancellation/reactivation continuity across checkout, portal, entitlement boundaries, and the shared commercial billing shell/model now used by both self-hosted Pulse Pro and hosted organization billing.", + "tracking": [] + }, + "blockers": [], + "subsystems": [ + "cloud-paid" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/cloud-paid.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/BillingAdminPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/BillingAdminPanel.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/CommercialBillingSections.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/commercialBillingModel.ts", + "kind": "file" + } + ] + }, + { + "id": "L4", + "name": "Hosted MSP readiness", + "target_score": 6, + "current_score": 6, + "status": "target-met", + "completion": { + "state": "complete", + "summary": "Hosted MSP support is at the current tracked RC floor, including real tenant-management behavior and the shared hosted commercial replay proof.", + "tracking": [] + }, + "blockers": [], + "subsystems": [], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-2026-03-13.md", + "kind": "file" + } + ] + }, + { + "id": "L5", + "name": "Mobile go-live readiness", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Mobile is at the RC usefulness floor; broader go-live hardening beyond pairing, auth, reconnect, and approvals remains post-RC follow-up.", + "tracking": [ + { + "kind": "lane-followup", + "id": "mobile-post-rc-hardening" + } + ] + }, + "blockers": [], + "subsystems": [], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/PRE_RELEASE_CHECKLIST.md", + "kind": "file" + }, + { + "repo": "pulse-mobile", + "path": "store/listing.md", + "kind": "file" + }, + { + "repo": "pulse-pro", + "path": "V6_LAUNCH_CHECKLIST.md", + "kind": "file" + } + ] + }, + { + "id": "L6", + "name": "Architecture coherence", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Architecture coherence now carries the canonical Connected infrastructure projection and frontend state contract by construction; the remaining residual is limited to later cross-surface cleanup outside those normalized contracts.", + "tracking": [ + { + "kind": "lane-followup", + "id": "architecture-post-rc-canonicalization" + } + ] + }, + "blockers": [], + "subsystems": [ + "ai-runtime", + "alerts", + "api-contracts", + "notifications", + "patrol-intelligence" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/ai-runtime.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/notifications.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/api/responseUtils.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/api/streaming.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/features/alerts/OverviewTab.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/alertIncidentPresentation.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/alertOverviewPresentation.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/resources.go", + "kind": "file" + }, + { + "repo": "pulse-enterprise", + "path": "docs/V6_REPO_REALIGNMENT.md", + "kind": "file" + } + ] + }, + { + "id": "L7", + "name": "Relay infrastructure readiness", + "target_score": 9, + "current_score": 9, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Relay infrastructure is at the RC floor, while broader hardening and non-blocking resilience work stay bounded to later promotion work.", + "tracking": [ + { + "kind": "lane-followup", + "id": "relay-post-rc-hardening" + } + ] + }, + "blockers": [], + "subsystems": [ + "relay-runtime" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/RELAY.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/relay", + "kind": "dir" + }, + { + "repo": "pulse-mobile", + "path": "src/relay", + "kind": "dir" + }, + { + "repo": "pulse-pro", + "path": "relay-server", + "kind": "dir" + } + ] + }, + { + "id": "L8", + "name": "First-session & UX polish", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "First-session UX reached the RC floor with the runtime wizard reduced to welcome and security, direct handoff into Infrastructure Operations install, and setup completion retained only as a separate preview surface; broader polish and parity work remain intentionally outside the current stabilization target.", + "tracking": [ + { + "kind": "lane-followup", + "id": "first-session-post-rc-polish" + } + ] + }, + "blockers": [], + "subsystems": [ + "frontend-primitives" + ], + "evidence": [ + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/Settings.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/SetupWizard/SetupWizard.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/11-first-session.spec.ts", + "kind": "file" + } + ] + }, + { + "id": "L9", + "name": "Documentation readiness", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Documentation is current enough for the RC floor, but GA-facing promotion and release communication stay as a lane-local follow-up alongside the shared RC-to-GA gate.", + "tracking": [ + { + "kind": "lane-followup", + "id": "documentation-ga-promotion-package" + }, + { + "kind": "release-gate", + "id": "rc-to-ga-promotion-readiness" + } + ] + }, + "blockers": [], + "subsystems": [], + "evidence": [ + { + "repo": "pulse", + "path": "docs", + "kind": "dir" + }, + { + "repo": "pulse", + "path": "docs/releases/RELEASE_NOTES_v6.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "README.md", + "kind": "file" + }, + { + "repo": "pulse-pro", + "path": "landing-page", + "kind": "dir" + } + ] + }, + { + "id": "L10", + "name": "Performance & scalability", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Performance is at the current release floor, but additional scalability headroom and non-blocking performance polish remain post-RC follow-up.", + "tracking": [ + { + "kind": "lane-followup", + "id": "performance-post-rc-headroom" + } + ] + }, + "blockers": [], + "subsystems": [ + "performance-and-scalability" + ], + "evidence": [ + { + "repo": "pulse", + "path": "frontend-modern/src/components/Dashboard/__tests__/Dashboard.performance.contract.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Dashboard/Dashboard.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Dashboard/workloadSelectors.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Infrastructure/__tests__/UnifiedResourceTable.performance.contract.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Infrastructure/infrastructureSelectors.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/http_metrics.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "pkg/metrics/store.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/02-navigation-perf.spec.ts", + "kind": "file" + } + ] + }, + { + "id": "L11", + "name": "v5-to-v6 migration safety", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Migration safety is at the RC floor, while GA promotion proof remains the only governed residual work.", + "tracking": [ + { + "kind": "release-gate", + "id": "rc-to-ga-promotion-readiness" + } + ] + }, + "blockers": [], + "subsystems": [], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/LEGACY_HOST_CLASSIFICATION_2026-03-05.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/V5_TO_V6_COMMERCIAL_MIGRATION_AUDIT_2026-03-07.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/UPGRADE_v6.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/scripts/sync-embed-dist.mjs", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/frontend_embed_sync_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/12-v5-commercial-migration.spec.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_commercial_migration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_full_upgrade_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_session_db_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/migration/v5_to_v6_test.go", + "kind": "file" + } + ] + }, + { + "id": "L12", + "name": "E2E journey coverage", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "The end-to-end journey suite covers the RC floor, while RC-to-GA promotion remains the only shared residual proof track.", + "tracking": [ + { + "kind": "release-gate", + "id": "rc-to-ga-promotion-readiness" + } + ] + }, + "blockers": [], + "subsystems": [], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/COMMERCIAL_CANCELLATION_REACTIVATION_E2E_TEST_PLAN.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/commercial-cancellation-reactivation-2026-03-12.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/journeys", + "kind": "dir" + } + ] + }, + { + "id": "L13", + "name": "Core monitoring runtime", + "target_score": 8, + "current_score": 8, + "status": "target-met", + "completion": { + "state": "complete", + "summary": "Core monitoring runtime is at the current tracked floor: discovery, metrics-history, and platform-runtime proof routing now land on explicit governed routes, while broader ReadState convergence remains owned by architecture coherence.", + "tracking": [] + }, + "blockers": [], + "subsystems": [ + "monitoring", + "unified-resources" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/monitoring.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/registry.json", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/unified-resources.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/TRUENAS.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/UNIFIED_RESOURCES.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Infrastructure/UnifiedResourceTable.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/resources.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/monitoring/canonical_guardrails_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/monitoring/metrics_history.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/monitoring/poll_providers.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/unifiedresources/code_standards_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/unifiedresources/metrics_targets.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/unifiedresources/registry.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "scripts/release_control/subsystem_lookup_test.py", + "kind": "file" + } + ] + }, + { + "id": "L14", + "name": "Security, identity, and privacy", + "target_score": 8, + "current_score": 8, + "status": "target-met", + "completion": { + "state": "complete", + "summary": "Security, identity, and privacy is at the current governed floor: privacy disclosures, telemetry controls, RBAC boundaries, and shared token/auth settings surfaces now route through explicit subsystem ownership with exercised proof across the remaining shared token and auth boundaries.", + "tracking": [] + }, + "blockers": [], + "subsystems": [ + "organization-settings", + "security-privacy" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/PRIVACY.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/organization-settings.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/security-privacy.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/api/__tests__/security.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/OrganizationSharingPanel.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/SecurityPostureSummary.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/organizationRolePresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/utils/__tests__/organizationSettingsPresentation.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/api_token_org_scope_integration_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/org_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/rbac_handlers_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_status_additional_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/security_tokens_owner_binding_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/system_settings_telemetry_test.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/telemetry/telemetry.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "SECURITY.md", + "kind": "file" + } + ] + }, + { + "id": "L15", + "name": "Storage and recovery", + "target_score": 8, + "current_score": 8, + "status": "partial", + "completion": { + "state": "bounded-residual", + "summary": "Storage and recovery have reached the governed RC floor. Filter coherence is proven across all four transport surfaces (rollups, points, series, facets), storage URL state now normalizes canonical source/status/node/resource selections, and recovery route-backed query, provider, outcome, and stale-only filters round-trip through shared links while facets still narrow with the selected timeline day. Broader storage health UX polish and additional recovery timeline hardening remain post-RC follow-up.", + "tracking": [ + { + "kind": "lane-followup", + "id": "storage-recovery-post-rc-hardening" + } + ] + }, + "blockers": [], + "subsystems": [ + "storage-recovery" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/RECOVERY.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/storage-recovery.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/STORAGE_ARCHITECTURE.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Recovery/__tests__/Recovery.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Recovery/Recovery.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Storage/__tests__/storagePageState.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Storage/Storage.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/recovery/recovery_test.go", + "kind": "file" + } + ] + }, + { + "id": "L16", + "name": "Agent lifecycle and fleet operations", + "target_score": 8, + "current_score": 8, + "status": "target-met", + "completion": { + "state": "complete", + "summary": "Agent lifecycle and fleet operations are at the current governed RC floor: canonical auto-register now converges on one v6 contract, install-command consumers fail closed through shared validated boundaries, profile-management surfaces preserve the canonical malformed-payload and missing-profile resync contract across settings and deploy surfaces, and the runtime-side Unified Agent reporting path now uses the canonical product terminology.", + "tracking": [] + }, + "blockers": [], + "subsystems": [ + "agent-lifecycle" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/unified-agent-v5-upgrade-continuity-2026-03-12.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/agent-lifecycle.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/api/agentProfiles.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/api/nodes.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/AgentProfilesPanel.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/NodeModal.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/agentupdate/update.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/config_setup_handlers.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/api/unified_agent.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/hostagent/agent.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "scripts/install.sh", + "kind": "file" + } + ] + } + ], + "release_gates": [ + { + "id": "api-token-scope-and-assignment", + "summary": "Confirm API tokens are assigned to the correct user and org context, enforce scope boundaries including legacy persisted host-agent scope aliases, and revoke cleanly.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "managed-runtime-exercise", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L1", + "L6", + "L11", + "L14", + "L16" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/api-token-scope-and-assignment-2026-03-12.md", + "kind": "file", + "evidence_tier": "managed-runtime-exercise" + } + ] + }, + { + "id": "cloud-hosted-tier-runtime-readiness", + "summary": "Confirm the actual hosted Pulse tier works end to end after provisioning: hosted auth, runtime entry, and hosted billing/admin surfaces all function coherently.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "blocked", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L3", + "L4", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-2026-03-13.md", + "kind": "file", + "evidence_tier": "local-rehearsal" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-blocked-2026-03-25.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-fixed-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/cloud-hosted-tier-runtime-readiness-production-followup-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + } + ] + }, + { + "id": "commercial-cancellation-reactivation", + "summary": "Confirm grandfathered v5 recurring continuity holds while active, completed cancellation revokes paid state, and re-entry after cancellation uses current public v6 pricing.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L2", + "L3", + "L11", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/commercial-cancellation-reactivation-2026-03-12.md", + "kind": "file", + "evidence_tier": "managed-runtime-exercise" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/commercial-cancellation-reactivation-external-e2e-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + } + ] + }, + { + "id": "documentation-currentness-and-legacy-cleanup", + "summary": "Confirm active v6-facing guidance is current, and any legacy or historical docs are clearly archived or demoted instead of remaining current guidance.", + "owner": "project-owner", + "blocking_level": "release-ready", + "minimum_evidence_tier": "local-rehearsal", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L9" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/documentation-currentness-and-legacy-cleanup-2026-03-13.md", + "kind": "file", + "evidence_tier": "local-rehearsal" + } + ] + }, + { + "id": "hosted-signup-billing-replay", + "summary": "Confirm hosted signup, org provisioning, billing-admin visibility, and webhook replay succeed in the real checkout path.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L2", + "L3", + "L4", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/hosted-signup-billing-replay-2026-03-12.md", + "kind": "file", + "evidence_tier": "local-rehearsal" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/hosted-signup-billing-replay-production-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/hosted-signup-billing-replay-production-fixed-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + } + ] + }, + { + "id": "mobile-relay-auth-approvals", + "summary": "Confirm pulse-mobile pairing, persistence, relay reconnect, auth transitions, and approval flows work against a real instance.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L5", + "L7", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/mobile-relay-auth-approvals-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + } + ] + }, + { + "id": "msp-provider-tenant-management", + "summary": "Confirm MSP mode behaves as a real provider workflow: one account can manage multiple client tenants coherently without cross-client leakage or MSP/public-plan confusion.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L3", + "L4", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-2026-03-13.md", + "kind": "file", + "evidence_tier": "local-rehearsal" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-production-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-production-fixed-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/msp-provider-tenant-management-production-followup-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + } + ] + }, + { + "id": "multi-tenant-runtime-isolation-and-coherence", + "summary": "Confirm multi-tenant Pulse behaves as a coherent tenant-isolated product across org scope, runtime state, sharing, and migration.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "managed-runtime-exercise", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L4", + "L6", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/multi-tenant-runtime-isolation-and-coherence-2026-03-13.md", + "kind": "file", + "evidence_tier": "managed-runtime-exercise" + } + ] + }, + { + "id": "organization-user-scope-and-rbac", + "summary": "Confirm user creation, organization membership, RBAC scope, and cross-org sharing fail closed outside intended access.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "managed-runtime-exercise", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L4", + "L9", + "L12", + "L14" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/organization-user-scope-and-rbac-2026-03-12.md", + "kind": "file", + "evidence_tier": "managed-runtime-exercise" + } + ] + }, + { + "id": "paid-feature-entitlement-gating", + "summary": "Confirm free-versus-paid feature gating and agent allocation accounting match entitlements across UI, API, hosted, and upgrade surfaces.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "managed-runtime-exercise", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L2", + "L3", + "L6", + "L9", + "L12", + "L16" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/paid-feature-entitlement-gating-2026-03-12.md", + "kind": "file", + "evidence_tier": "managed-runtime-exercise" + } + ] + }, + { + "id": "rc-to-ga-promotion-readiness", + "summary": "Confirm stable or GA promotion is coming from an exercised RC with live release-pipeline proof, rollback instructions, and a written v5 maintenance-only policy.", + "owner": "project-owner", + "blocking_level": "release-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "blocked", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L1", + "L9", + "L11", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/rc-to-ga-promotion-readiness-blocked-2026-03-13.md", + "kind": "file", + "evidence_tier": "local-rehearsal" + } + ] + }, + { + "id": "relay-registration-reconnect-drain", + "summary": "Confirm relay fresh registration, reconnect, stale-session recovery, and disconnect drain behavior hold under real pressure.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "managed-runtime-exercise", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L5", + "L7", + "L12" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/relay-registration-reconnect-drain-2026-03-13.md", + "kind": "file", + "evidence_tier": "managed-runtime-exercise" + } + ] + }, + { + "id": "settings-surface-layout-consistency", + "summary": "Confirm comparable settings surfaces present the canonical page shell, with consistent header framing and no ad hoc top-level layout chrome.", + "owner": "project-owner", + "blocking_level": "release-ready", + "minimum_evidence_tier": "local-rehearsal", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L8" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/settings-surface-layout-consistency-2026-03-13.md", + "kind": "file", + "evidence_tier": "local-rehearsal" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts", + "kind": "file", + "evidence_tier": "test-proof" + } + ] + }, + { + "id": "unified-agent-v5-upgrade-continuity", + "summary": "Confirm a real v5-installed Pulse Unified Agent upgrades through candidate v6 RC assets into one canonical v6 agent identity without duplicate registration, stale fallback or legacy-scope breakage, or agent-count drift.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L11", + "L12", + "L16" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/unified-agent-v5-upgrade-continuity-2026-03-12.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + } + ] + }, + { + "id": "upgrade-state-and-entitlement-preservation", + "summary": "Confirm supported upgrades preserve state, entitlements, and first-session continuity without repair flows.", + "owner": "project-owner", + "blocking_level": "rc-ready", + "minimum_evidence_tier": "real-external-e2e", + "status": "passed", + "verification_doc": "docs/release-control/v6/internal/HIGH_RISK_RELEASE_VERIFICATION_MATRIX.md", + "lane_ids": [ + "L3", + "L5", + "L8" + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/upgrade-state-and-entitlement-preservation-2026-03-13.md", + "kind": "file", + "evidence_tier": "real-external-e2e" + } + ] + } + ], + "lane_followups": [ + { + "id": "architecture-post-rc-canonicalization", + "summary": "Track the remaining post-RC architecture cleanup that still belongs to L6 after canonicalizing Connected infrastructure and the frontend state contract by construction.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-13", + "lane_ids": [ + "L6" + ], + "subsystem_ids": [ + "ai-runtime", + "alerts", + "notifications", + "patrol-intelligence" + ] + }, + { + "id": "documentation-ga-promotion-package", + "summary": "Track the GA-facing documentation and release communication package that should only be finalized when promotion is actually being executed.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-13", + "lane_ids": [ + "L9" + ], + "subsystem_ids": [] + }, + { + "id": "first-session-post-rc-polish", + "summary": "Track broader first-session polish and parity work that is intentionally outside the RC stabilization floor.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-13", + "lane_ids": [ + "L8" + ], + "subsystem_ids": [ + "frontend-primitives" + ] + }, + { + "id": "mobile-post-rc-hardening", + "summary": "Track broader mobile go-live hardening beyond the RC usefulness floor, including work above pairing, auth, reconnect, and approvals recovery.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-13", + "lane_ids": [ + "L5" + ], + "subsystem_ids": [] + }, + { + "id": "performance-post-rc-headroom", + "summary": "Track additional scalability headroom and non-blocking performance polish beyond the current release floor.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-13", + "lane_ids": [ + "L10" + ], + "subsystem_ids": [ + "performance-and-scalability" + ] + }, + { + "id": "relay-post-rc-hardening", + "summary": "Track broader relay hardening and non-blocking resilience work that remains outside the current RC floor, including any future narrowing of the dedicated backend-owned mobile relay capability beyond the current runtime route set.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-13", + "lane_ids": [ + "L7" + ], + "subsystem_ids": [ + "relay-runtime" + ] + }, + { + "id": "self-hosted-ga-promotion-package", + "summary": "Track the lane-local self-hosted GA promotion package: stable candidate inputs, rollback clarity, and dry-run capture once GA promotion is actually being executed.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-13", + "lane_ids": [ + "L1" + ], + "subsystem_ids": [] + }, + { + "id": "storage-recovery-post-rc-hardening", + "summary": "Track broader storage health UX polish and recovery timeline hardening beyond the current RC floor, including additional filter coherence edge cases and storage panel presentation improvements.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-14", + "lane_ids": [ + "L15" + ], + "repo_ids": [ + "pulse" + ], + "subsystem_ids": [ + "storage-recovery" + ], + "cross_repo": false + } + ], + "coverage_gaps": [ + { + "id": "action-governance-and-audit", + "summary": "v6 has approvals and AI surfaces, but it lacks a governed action model covering declared capabilities, planning and dry-run boundaries, approval requirements, audit trail ownership, and safe execution contracts.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-17", + "lane_ids": [ + "L6", + "L14" + ], + "subsystem_ids": [ + "ai-runtime", + "api-contracts", + "patrol-intelligence", + "security-privacy" + ], + "proposed_resolution": "new-lane", + "coverage_impact": 14, + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/ai-runtime.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/V6_BRIDGE_RELEASE_FOUNDATION_SPEC.md", + "kind": "file" + } + ] + }, + { + "id": "fleet-governance-v1", + "summary": "L16 covers install and registration continuity, but the governed map still underrepresents fleet governance primitives such as enrollment state, version drift, adapter health, config rollout, credential status, and remote control-plane safety.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-17", + "lane_ids": [ + "L16" + ], + "subsystem_ids": [ + "agent-lifecycle" + ], + "proposed_resolution": "lane-expansion", + "coverage_impact": 10, + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/agent-lifecycle.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/V6_BRIDGE_RELEASE_FOUNDATION_SPEC.md", + "kind": "file" + } + ] + }, + { + "id": "policy-aware-data-governance", + "summary": "The current v6 map treats privacy and AI runtime as adjacent concerns, but it does not yet govern sensitivity classification, redaction, local-vs-cloud routing, or AI-safe summary boundaries as a first-class release surface.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-17", + "lane_ids": [ + "L6", + "L14" + ], + "subsystem_ids": [ + "ai-runtime", + "api-contracts", + "security-privacy" + ], + "proposed_resolution": "new-lane", + "coverage_impact": 15, + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/security-privacy.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/V6_BRIDGE_RELEASE_FOUNDATION_SPEC.md", + "kind": "file" + } + ] + }, + { + "id": "resource-change-and-timeline", + "summary": "The current v6 lane map proves unified resources and monitoring floors, but the resource-change and cross-resource timeline work is now kept as hidden backend foundation for investigation and AI flows until the surfaced case is proven.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-17", + "lane_ids": [ + "L6", + "L13" + ], + "subsystem_ids": [ + "alerts", + "api-contracts", + "monitoring", + "unified-resources" + ], + "proposed_resolution": "lane-split", + "coverage_impact": 15, + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/unified-resources.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/V6_BRIDGE_RELEASE_FOUNDATION_SPEC.md", + "kind": "file" + } + ] + }, + { + "id": "customer-account-portal-surface", + "summary": "v6 has real billing, checkout, hosted tenant, MSP account, and self-serve utility surfaces, but it still lacks one coherent authenticated Pulse account portal that unifies licenses, hosted tenants, billing, recovery, and MSP admin actions under a canonical customer control surface.", + "owner": "project-owner", + "status": "planned", + "recorded_at": "2026-03-25", + "lane_ids": [ + "L3", + "L4" + ], + "subsystem_ids": [ + "cloud-paid" + ], + "proposed_resolution": "new-lane", + "coverage_impact": 14, + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/PULSE_ACCOUNT_PORTAL_SPEC.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/subsystems/cloud-paid.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/cloudcp/account/tenant_handlers.go", + "kind": "file" + }, + { + "repo": "pulse", + "path": "internal/cloudcp/portal/page.go", + "kind": "file" + }, + { + "repo": "pulse-pro", + "path": "landing-page/manage.html", + "kind": "file" + }, + { + "repo": "pulse-pro", + "path": "landing-page/retrieve-license.html", + "kind": "file" + } + ] + } + ], + "candidate_lanes": [ + { + "id": "action-governance-auditability", + "name": "Action governance and auditability", + "summary": "Promote action capability declaration, plan and dry-run state, approval requirements, safe execution boundaries, and audit ownership into a dedicated governed lane.", + "status": "planned", + "recorded_at": "2026-03-17", + "target_id": "v6-product-lane-expansion", + "current_lane_ids": [ + "L6", + "L14" + ], + "coverage_gap_ids": [ + "action-governance-and-audit" + ], + "subsystem_ids": [ + "ai-runtime", + "api-contracts", + "patrol-intelligence", + "security-privacy" + ] + }, + { + "id": "fleet-governance-rollout-control", + "name": "Fleet governance and rollout control", + "summary": "Expand the current fleet lane from install and lifecycle continuity into governed enrollment, drift, adapter-health, config-rollout, and credential-status control surfaces.", + "status": "planned", + "recorded_at": "2026-03-17", + "target_id": "v6-product-lane-expansion", + "current_lane_ids": [ + "L16" + ], + "coverage_gap_ids": [ + "fleet-governance-v1" + ], + "subsystem_ids": [ + "agent-lifecycle" + ] + }, + { + "id": "policy-aware-data-governance-lane", + "name": "Policy-aware data governance", + "summary": "Promote sensitivity classification, redaction, local-vs-cloud routing, and AI-safe summary boundaries into a governed product lane rather than leaving them split across privacy and AI runtime residue.", + "status": "planned", + "recorded_at": "2026-03-17", + "target_id": "v6-product-lane-expansion", + "current_lane_ids": [ + "L6", + "L14" + ], + "coverage_gap_ids": [ + "policy-aware-data-governance" + ], + "subsystem_ids": [ + "ai-runtime", + "api-contracts", + "security-privacy" + ] + }, + { + "id": "resource-change-intelligence", + "name": "Resource change intelligence", + "summary": "Promote canonical resource relationships, first-class change envelopes, and cross-resource timelines into an explicit lane once the monitoring-first surface proves the case; keep the backend foundations hidden until then.", + "status": "planned", + "recorded_at": "2026-03-17", + "target_id": "v6-product-lane-expansion", + "current_lane_ids": [ + "L6", + "L13" + ], + "coverage_gap_ids": [ + "resource-change-and-timeline" + ], + "subsystem_ids": [ + "alerts", + "api-contracts", + "monitoring", + "unified-resources" + ] + }, + { + "id": "customer-account-portal", + "name": "Customer account portal", + "summary": "Promote fragmented cloud billing, hosted tenant, self-serve licensing, recovery, and MSP admin surfaces into one governed Pulse account lane with a coherent customer and operator control surface.", + "status": "planned", + "recorded_at": "2026-03-25", + "target_id": "v6-product-lane-expansion", + "current_lane_ids": [ + "L3", + "L4" + ], + "coverage_gap_ids": [ + "customer-account-portal-surface" + ], + "subsystem_ids": [ + "cloud-paid" + ] + } + ], + "work_claims": [ + { + "id": "codex-candidate-lane-customer-account-portal", + "agent_id": "codex", + "summary": "Define the canonical Pulse Account portal lane, IA, and subsystem ownership", + "target_id": "v6-product-lane-expansion", + "claimed_at": "2026-03-25T22:18:45Z", + "heartbeat_at": "2026-03-25T22:18:45Z", + "expires_at": "2026-03-26T00:18:45Z", + "work_item": { + "kind": "candidate-lane", + "id": "customer-account-portal" + } + } + ], + "open_decisions": [], + "source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", + "resolved_decisions": [ + { + "id": "ga-floor-policy", + "summary": "Staged release policy locked; L4 Hosted MSP full portal is post-GA and not a GA floor gate.", + "kind": "release-policy", + "decided_at": "2026-02-27", + "subsystem_ids": [], + "lane_ids": [ + "L4" + ] + }, + { + "id": "msp-pricing-band-lock", + "summary": "MSP pricing locked at Starter/Growth/Scale = $149/$249/$399 monthly with 10/25/50 workspace breakpoints.", + "kind": "pricing", + "decided_at": "2026-02-27", + "subsystem_ids": [], + "lane_ids": [ + "L2", + "L4" + ] + }, + { + "id": "stripe-mapping-contract-lock", + "summary": "Cloud/MSP Stripe mapping contract is locked; concrete price IDs remain operational fill-in tasks.", + "kind": "contract", + "decided_at": "2026-02-27", + "subsystem_ids": [ + "cloud-paid" + ], + "lane_ids": [ + "L2", + "L3", + "L4" + ] + }, + { + "id": "host-type-migration-boundary-audit", + "summary": "Host-type migration audit completed; remaining host references are compatibility boundaries, internal shims, or non-resource terminology.", + "kind": "architecture", + "decided_at": "2026-03-05", + "subsystem_ids": [ + "agent-lifecycle", + "alerts", + "api-contracts", + "monitoring", + "unified-resources" + ], + "lane_ids": [ + "L6", + "L11", + "L13", + "L16" + ] + }, + { + "id": "trial-authority-saas-controlled", + "summary": "Trial authority for v6 is SaaS-controlled; hosted signup initiates trial start and local runtime may only redeem signed trial activations.", + "kind": "contract", + "decided_at": "2026-03-06", + "subsystem_ids": [ + "cloud-paid" + ], + "lane_ids": [ + "L2", + "L3", + "L11" + ] + }, + { + "id": "v5-license-bridge-landed", + "summary": "The v5-to-v6 license migration bridge landed, including upgrade exchange, activation input acceptance, and aligned public migration guidance.", + "kind": "migration", + "decided_at": "2026-03-06", + "subsystem_ids": [], + "lane_ids": [ + "L11" + ] + }, + { + "id": "browser-proof-commercial-migration", + "summary": "Browser-level proof exists for unresolved v5 commercial migration states via the upgraded-fixture commercial migration spec.", + "kind": "migration", + "decided_at": "2026-03-07", + "subsystem_ids": [], + "lane_ids": [ + "L11" + ] + }, + { + "id": "commercial-migration-truth-table-owned", + "summary": "The v5 commercial migration truth table is owned by v6 release-control and unresolved paid-license states must remain preserved through migration.", + "kind": "migration", + "decided_at": "2026-03-07", + "subsystem_ids": [], + "lane_ids": [ + "L11" + ] + }, + { + "id": "embedded-frontend-drift-protection", + "summary": "Embedded frontend drift protection landed through synced dist assets, embed parity tests, and native embedded migration UI proof.", + "kind": "governance", + "decided_at": "2026-03-07", + "subsystem_ids": [], + "lane_ids": [ + "L11" + ] + }, + { + "id": "orchestrator-retired", + "summary": "The v6 release-control orchestrator was retired; direct repo-aware sessions and governance guardrails are the only supported execution path.", + "kind": "governance", + "decided_at": "2026-03-11", + "subsystem_ids": [], + "lane_ids": [ + "L6", + "L9" + ] + }, + { + "id": "top-level-governance-split", + "summary": "SOURCE_OF_TRUTH.md now owns only stable governance and locked decisions; live lane state and evidence references live only in status.json.", + "kind": "governance", + "decided_at": "2026-03-11", + "subsystem_ids": [], + "lane_ids": [ + "L6", + "L9" + ] + }, + { + "id": "stable-release-promotion-model", + "summary": "v6 and later releases use an opt-in RC preview channel plus a promotion-only stable channel; stable customers must receive only already-validated builds, unattended auto-update exposure remains stable-only, and GA stays blocked until rollback instructions and the v5 maintenance-only policy are explicit.", + "kind": "release-policy", + "decided_at": "2026-03-12", + "subsystem_ids": [], + "lane_ids": [ + "L1", + "L3", + "L11", + "L12" + ] + }, + { + "id": "v5-maintenance-support-policy", + "summary": "Once v6 reaches stable or GA, v5 moves to a 90-day maintenance-only window for critical security issues, critical correctness/data-loss issues, and safe migration blockers only; after that window, v5 is unsupported.", + "kind": "release-policy", + "decided_at": "2026-03-12", + "subsystem_ids": [], + "lane_ids": [ + "L9" + ] + }, + { + "id": "v5-pro-price-grandfathering", + "summary": "Paid Pulse Pro v5 customers keep their existing recurring price through the v6 pricing change until they cancel; renewals must preserve that grandfathered price state, while any return after cancellation re-enters on current v6 pricing.", + "kind": "pricing", + "decided_at": "2026-03-12", + "subsystem_ids": [ + "cloud-paid" + ], + "lane_ids": [ + "L2", + "L3", + "L11" + ] + }, + { + "id": "cloud-msp-price-id-propagation", + "summary": "The 13 canonical Cloud/MSP v6 price IDs are populated in the governed pulse-pro operations doc, launch checklist, and license-server env mapping template.", + "kind": "contract", + "decided_at": "2026-03-13", + "subsystem_ids": [ + "cloud-paid" + ], + "lane_ids": [ + "L2", + "L3", + "L4" + ] + }, + { + "id": "cloud-msp-stripe-prices", + "summary": "The 13 canonical Cloud/MSP v6 price IDs already exist as active live recurring Stripe prices and match the governed commercial mappings.", + "kind": "pricing", + "decided_at": "2026-03-13", + "subsystem_ids": [ + "cloud-paid" + ], + "lane_ids": [ + "L2", + "L3", + "L4" + ] + }, + { + "id": "mobile-usefulness-floor", + "summary": "Pulse Mobile is considered useful enough for the v6 RC line when it preserves at least one trusted paired instance across relaunches, surfaces relay/runtime state clearly in the main shell, fails closed into a recoverable disconnected state on stale or revoked access, and supports live approval visibility and action recovery; broader parity remains post-RC scope.", + "kind": "release-policy", + "decided_at": "2026-03-13", + "subsystem_ids": [ + "frontend-primitives", + "relay-runtime" + ], + "lane_ids": [ + "L5", + "L7", + "L8", + "L12" + ] + }, + { + "id": "accidental-prerelease-tags-do-not-count-as-shipped-rcs", + "summary": "Accidental prerelease git tags do not count as shipped prerelease lineage. `v6.0.0-rc.1` was pushed accidentally, never published, and must not be treated as a valid stable-promotion `promoted_from_tag`.", + "kind": "release-policy", + "decided_at": "2026-03-14", + "subsystem_ids": [], + "lane_ids": [ + "L1", + "L9", + "L11", + "L12" + ] + }, + { + "id": "monitored-systems-counting-contract", + "summary": "Commercial counting for Pulse v6 is based on monitored systems, not installed agents: each top-level monitored system counts once regardless of collection path, while child resources remain included and API-only monitoring must consume the same cap as agent-backed monitoring.", + "kind": "contract", + "decided_at": "2026-03-17", + "subsystem_ids": [], + "lane_ids": [ + "L2" + ] + }, + { + "id": "pulse-not-agent-sandbox-boundary", + "summary": "Pulse is not a universal agent sandbox; it is the infrastructure-specific context, policy, and action plane that sandboxed agents should use.", + "kind": "architecture", + "decided_at": "2026-03-17", + "subsystem_ids": [ + "ai-runtime", + "api-contracts", + "security-privacy", + "unified-resources" + ], + "lane_ids": [ + "L6", + "L13", + "L14" + ] + }, + { + "id": "self-hosted-pricing-band-lock-v2", + "summary": "Pulse v6 self-hosted pricing is locked at Community free, Relay $4.99/$39, Pro $8.99/$79, and Pro+ $14.99/$129 with the 5/8/15/50 monitored-system ladder.", + "kind": "pricing", + "decided_at": "2026-03-17", + "subsystem_ids": [], + "lane_ids": [ + "L2" + ] + }, + { + "id": "v6-bridge-release-foundation", + "summary": "Pulse v6 is a bridge release toward a resource + policy + control platform: it must land irreversible primitives for canonical resources, policy-aware routing, governed actions, and fleet control without attempting the full private operational broker in one release.", + "kind": "architecture", + "decided_at": "2026-03-17", + "subsystem_ids": [ + "agent-lifecycle", + "ai-runtime", + "api-contracts", + "security-privacy", + "unified-resources" + ], + "lane_ids": [ + "L6", + "L13", + "L14", + "L16" + ] + }, + { + "id": "canonical-timeline-source-precedence", + "summary": "Unified-resource change history is the canonical durable backend timeline; alert incident memory remains a derived investigation projection for alert-local notes, analysis, commands, runbooks, and lifecycle breadcrumbs.", + "kind": "architecture", + "decided_at": "2026-03-20", + "subsystem_ids": [ + "ai-runtime", + "alerts", + "api-contracts", + "monitoring", + "unified-resources" + ], + "lane_ids": [ + "L6", + "L13" + ] + } + ] +} diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 3100b90e2..26dafc3e3 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -176,10 +176,11 @@ inventory export route for reporting. Fleet and install surfaces may coexist with that export, but `internal/api/reporting_inventory_handlers.go` and `internal/api/router_routes_licensing.go` remain API-owned reporting transport, not lifecycle-owned inventory or install behavior. -That adjacent reporting transport now also includes a VM inventory definition -route that owns export title, column schema, and filename prefix. Lifecycle- +That adjacent reporting transport now also includes a reporting catalog route +plus a VM inventory definition route that own panel copy, performance report +options, export title, column schema, and filename prefixes. Lifecycle- adjacent install and fleet surfaces may read those facts, but they must not -redefine inventory schema locally. +redefine reporting or inventory schema locally. That adjacent export contract now also carries canonical Proxmox pool membership for each VM row. Lifecycle-adjacent install and fleet surfaces may reuse those current-state facts, but they must still treat the pool column as diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 25eae9ec6..f8ee15973 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -217,14 +217,16 @@ The reporting API contract now also treats current-state fleet inventory as a first-class surface separate from historical metrics reports. `internal/api/reporting_inventory_handlers.go`, `internal/api/router_routes_licensing.go`, and the settings reporting shell now -own `/api/admin/reports/inventory/vms/definition` plus -`/api/admin/reports/inventory/vms/export` as the canonical VM inventory -contract. The definition endpoint owns the operator-facing title, description, -filename prefix, and stable column schema, while the export endpoint remains -the spreadsheet-shaped CSV transport. That export is intentionally not comment- -prefixed like the legacy metrics CSV, and it now carries Proxmox pool -membership from the canonical unified VM runtime model instead of inferring or -reconstructing that field locally inside the frontend or handler. +own `/api/admin/reports/catalog` as the canonical operator-facing reporting +catalog plus `/api/admin/reports/inventory/vms/definition` and +`/api/admin/reports/inventory/vms/export` as the stable VM inventory sub- +contract. The catalog endpoint owns the reporting panel title, description, +historical performance report options, and nested VM inventory definition, +while the export endpoint remains the spreadsheet-shaped CSV transport. That +export is intentionally not comment-prefixed like the legacy metrics CSV, and +it now carries Proxmox pool membership from the canonical unified VM runtime +model instead of inferring or reconstructing that field locally inside the +frontend or handler. The `/api/resources` serializer now also refreshes canonical identity and policy metadata through the shared unified-resource helper before it writes the payload, so backend and frontend contract tests stay aligned on one diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 2d2856b85..914d09152 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -169,16 +169,18 @@ The settings reporting shell now also owns a deliberate split between historical performance reports and current-state VM inventory export. `frontend-modern/src/components/Settings/ReportingPanel.tsx`, `frontend-modern/src/components/Settings/useReportingPanelState.ts`, +`frontend-modern/src/components/Settings/reportingCatalogModel.ts`, `frontend-modern/src/components/Settings/reportingPanelModel.ts`, and `frontend-modern/src/components/Settings/reportingInventoryExportModel.ts` must keep those as separate operator jobs with separate request builders and success copy, rather than collapsing inventory export back into the metrics-report controls. -That same settings shell must now also render VM inventory export schema from -the backend-owned definition contract rather than hardcoding column copy in the -panel. The frontend model may validate and present the definition, but the -canonical title, description, filename prefix, and column list belong to the -API reporting contract. +That same settings shell must now also render both historical performance +options and VM inventory schema from the backend-owned reporting catalog rather +than hardcoding panel copy, routes, or range presets in the frontend. The +frontend models may validate and present the catalog, but the canonical panel +title, descriptions, endpoints, filename prefixes, range windows, and column +list belong to the API reporting contract. The shared updates settings owner also defines the user-facing framing for rc-tagged builds. `frontend-modern/src/components/Settings/updatesSettingsModel.ts` and `frontend-modern/src/utils/updatesPresentation.ts` must present that diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 500c869cc..2ac729472 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -222,10 +222,11 @@ reporting surface. Storage and recovery workflows may consume similar current- state VM facts, but `internal/api/reporting_inventory_handlers.go` and `internal/api/router_routes_licensing.go` remain API/reporting transport ownership rather than storage/recovery contract ownership. -That adjacent reporting transport now also includes a VM inventory definition -route that owns export title, stable column schema, and filename prefix. -Storage and recovery flows may read those facts when they need fleet context, -but they must not fork their own inventory column contract. +That adjacent reporting transport now also includes a reporting catalog route +plus a VM inventory definition route that own panel copy, stable column +schema, and filename prefix. Storage and recovery flows may read those facts +when they need fleet context, but they must not fork their own reporting or +inventory column contract. That adjacent export contract now also includes canonical Proxmox pool membership for each VM row. Storage and recovery flows may use those current- state facts when they need fleet context, but they must consume the API-owned diff --git a/frontend-modern/src/components/Settings/ReportingPanel.tsx b/frontend-modern/src/components/Settings/ReportingPanel.tsx index e5e6e990b..a8ff43c67 100644 --- a/frontend-modern/src/components/Settings/ReportingPanel.tsx +++ b/frontend-modern/src/components/Settings/ReportingPanel.tsx @@ -1,34 +1,28 @@ -import { For, Show, JSX } from 'solid-js'; +import { For, JSX, Show } from 'solid-js'; import FileText from 'lucide-solid/icons/file-text'; import Download from 'lucide-solid/icons/download'; import BarChart from 'lucide-solid/icons/bar-chart'; import TableProperties from 'lucide-solid/icons/table-properties'; import OperationsPanel from '@/components/Settings/OperationsPanel'; import { CalloutCard } from '@/components/shared/CalloutCard'; -import { formField, formLabel, formHelpText, formControl } from '@/components/shared/Form'; +import { formControl, formField, formHelpText, formLabel } from '@/components/shared/Form'; import { FilterButtonGroup, type FilterOption } from '@/components/shared/FilterButtonGroup'; -import { ResourcePicker } from './ResourcePicker'; +import { useReportingPanelState } from '@/components/Settings/useReportingPanelState'; +import type { ReportingFormat } from '@/components/Settings/reportingCatalogModel'; +import { type ReportingRangeValue } from '@/components/Settings/reportingPanelModel'; import { trackUpgradeClicked } from '@/utils/upgradeMetrics'; -import { REPORTING_RANGE_OPTIONS } from '@/utils/reportingPresentation'; import { getUpgradeActionButtonClass, UPGRADE_ACTION_LABEL, UPGRADE_TRIAL_LABEL, UPGRADE_TRIAL_LINK_CLASS, } from '@/utils/upgradePresentation'; -import { useReportingPanelState } from '@/components/Settings/useReportingPanelState'; -import { type ReportingRangeValue } from '@/components/Settings/reportingPanelModel'; +import { ResourcePicker } from './ResourcePicker'; -const REPORTING_RANGE_FILTER_OPTIONS: FilterOption[] = - REPORTING_RANGE_OPTIONS.map((option) => ({ - label: option.label, - value: option.value, - })); - -const REPORTING_FORMAT_FILTER_OPTIONS: FilterOption<'pdf' | 'csv'>[] = [ - { value: 'pdf', label: 'PDF Report', icon: FileText }, - { value: 'csv', label: 'CSV Data', icon: BarChart }, -]; +const REPORTING_FORMAT_ICONS: Record = { + csv: BarChart, + pdf: FileText, +}; interface FormFieldProps { label: string; @@ -55,13 +49,13 @@ export function ReportingPanel() { generating, handleGenerate, handleStartTrial, - inventoryDefinition, - inventoryDefinitionError, - inventoryDefinitionLoading, isLocked, isReportingEnabled, metricType, range, + reportingCatalog, + reportingCatalogError, + reportingCatalogLoading, selectedResources, setFormat, setMetricType, @@ -73,12 +67,31 @@ export function ReportingPanel() { upgradeActionUrl, } = useReportingPanelState(); + const performanceReport = () => reportingCatalog()?.performanceReport ?? null; + const inventoryDefinition = () => reportingCatalog()?.vmInventoryExport ?? null; + + const rangeFilterOptions = (): FilterOption[] => + (performanceReport()?.ranges ?? []).map((option) => ({ + label: option.label, + value: option.key, + })); + + const formatFilterOptions = (): FilterOption[] => + (performanceReport()?.formats ?? []).map((option) => ({ + value: option.value, + label: option.label, + icon: REPORTING_FORMAT_ICONS[option.value], + })); + return (
} >
@@ -120,144 +133,155 @@ export function ReportingPanel() { } >
-
-
-

Performance Reports

-

- Generate PDF summaries or CSV metric exports from historical monitoring data for - one or more selected resources. -

-
+ +

Loading reporting surfaces...

+
- - - + +

{reportingCatalogError()}

+
-
- - setMetricType(e.currentTarget.value)} - /> - - - - setTitle(e.currentTarget.value)} - /> - -
- -
- - - - - - - -
- -
- -
-
- -
-
-

VM Inventory Export

-

- {inventoryDefinition()?.description ?? - 'Export the current fleet-wide VM inventory as CSV using the canonical runtime model.'} -

-
- - -

Loading export column definition...

-
- - -

{inventoryDefinitionError()}

-
- - -
- - {(column) => ( -
-
- {column.label} -
-

{column.description}

-
- )} -
+ +
+
+

+ {performanceReport()?.title} +

+

{performanceReport()?.description}

- -
- -
-
+ + + +
+ + setMetricType(e.currentTarget.value)} + /> + + + + setTitle(e.currentTarget.value)} + /> + +
+ +
+ + + + + + + +
+ +
+ +
+
+ + + +
+
+

+ {inventoryDefinition()?.title} +

+

{inventoryDefinition()?.description}

+
+ + +
+ + {(column) => ( +
+
+ {column.label} +
+

{column.description}

+
+ )} +
+
+
+ +
+ +
+
+
diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts index c91ad7e1d..608ca88ab 100644 --- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts @@ -61,6 +61,7 @@ import aiRuntimeControlsSectionSource from '../AIRuntimeControlsSection.tsx?raw' import aiSettingsStatusAndActionsSource from '../AISettingsStatusAndActions.tsx?raw'; import aiSettingsStateSource from '../useAISettingsState.ts?raw'; import reportingPanelSource from '../ReportingPanel.tsx?raw'; +import reportingCatalogModelSource from '../reportingCatalogModel.ts?raw'; import reportingPanelModelSource from '../reportingPanelModel.ts?raw'; import reportingInventoryExportModelSource from '../reportingInventoryExportModel.ts?raw'; import updatesSettingsPanelSource from '../UpdatesSettingsPanel.tsx?raw'; @@ -598,14 +599,14 @@ describe('monitored-system model guardrails', () => { expect(agentProfileSuggestionPresentationSource).toContain( 'export function getAgentProfileSuggestionRiskHints', ); - expect(reportingPanelSource).toContain('REPORTING_RANGE_OPTIONS'); expect(reportingPanelSource).toContain('FilterButtonGroup'); expect(reportingPanelSource).toContain('CalloutCard'); expect(reportingPanelSource).toContain('variant="prominent"'); expect(reportingPanelSource).toContain('@/utils/upgradePresentation'); expect(reportingPanelSource).toContain('@/components/Settings/useReportingPanelState'); + expect(reportingPanelSource).toContain('@/components/Settings/reportingCatalogModel'); expect(reportingPanelSource).toContain('@/components/Settings/reportingPanelModel'); - expect(reportingPanelSource).toContain('VM Inventory Export'); + expect(reportingPanelSource).toContain('reportingCatalog'); expect(reportingPanelSource).toContain('getUpgradeActionButtonClass'); expect(reportingPanelSource).toContain('UPGRADE_ACTION_LABEL'); expect(reportingPanelSource).toContain('UPGRADE_TRIAL_LABEL'); @@ -614,17 +615,20 @@ describe('monitored-system model guardrails', () => { expect(reportingPanelSource).not.toContain(""); expect(reportingPanelSource).not.toContain('window.URL.createObjectURL'); expect(reportingPanelStateSource).toContain('buildReportingRequest'); + expect(reportingPanelStateSource).toContain('buildReportingCatalogRequest'); + expect(reportingPanelStateSource).toContain('parseReportingCatalog'); expect(reportingPanelStateSource).toContain('getReportingGenerateSelectionRequiredMessage'); expect(reportingPanelStateSource).toContain('getReportingGenerateSuccessMessage'); expect(reportingPanelStateSource).toContain('getReportingGenerateErrorMessage'); expect(reportingPanelStateSource).toContain('buildVMInventoryExportRequest'); expect(reportingPanelStateSource).toContain('getReportingInventoryExportSuccessMessage'); + expect(reportingCatalogModelSource).toContain('export function buildReportingCatalogRequest'); + expect(reportingCatalogModelSource).toContain('export function parseReportingCatalog'); expect(reportingPanelModelSource).toContain('export function getReportingRangeStart'); expect(reportingPanelModelSource).toContain('export function buildReportingRequest'); expect(reportingInventoryExportModelSource).toContain( 'export function buildVMInventoryExportRequest', ); - expect(reportingPresentationSource).toContain('export const REPORTING_RANGE_OPTIONS'); expect(reportingPresentationSource).toContain( 'export function getReportingGenerateSelectionRequiredMessage', ); diff --git a/frontend-modern/src/components/Settings/__tests__/reportingCatalogModel.test.ts b/frontend-modern/src/components/Settings/__tests__/reportingCatalogModel.test.ts new file mode 100644 index 000000000..f93ed3dda --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/reportingCatalogModel.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { + buildReportingCatalogRequest, + parseReportingCatalog, +} from '../reportingCatalogModel'; + +describe('reporting catalog model', () => { + it('builds the canonical reporting catalog request', () => { + expect(buildReportingCatalogRequest()).toEqual({ + url: '/api/admin/reports/catalog', + }); + }); + + it('parses the canonical reporting catalog payload', () => { + const catalog = parseReportingCatalog({ + id: 'advanced_reporting', + title: 'Detailed Reporting', + description: 'Canonical reporting surfaces', + performanceReport: { + id: 'performance_reports', + title: 'Performance Reports', + description: 'Historical performance reporting', + singleResourceEndpoint: '/api/admin/reports/generate', + multiResourceEndpoint: '/api/admin/reports/generate-multi', + singleFilenamePrefix: 'report', + multiFilenamePrefix: 'fleet-report', + formats: [ + { value: 'pdf', label: 'PDF Report' }, + { value: 'csv', label: 'CSV Data' }, + ], + defaultFormat: 'pdf', + ranges: [ + { + key: '24h', + label: 'Last 24 Hours', + description: 'Daily review', + windowHours: 24, + }, + ], + defaultRange: '24h', + multiResourceMax: 50, + supportsMetricFilter: true, + supportsCustomTitle: true, + }, + vmInventoryExport: { + id: 'vm_inventory', + title: 'VM Inventory Export', + description: 'Current-state inventory', + format: 'csv', + exportEndpoint: '/api/admin/reports/inventory/vms/export', + filenamePrefix: 'vm-inventory', + columns: [ + { + key: 'pool', + label: 'Pool', + description: 'Canonical Proxmox pool membership.', + }, + ], + }, + }); + + expect(catalog.performanceReport.defaultFormat).toBe('pdf'); + expect(catalog.performanceReport.ranges[0].windowHours).toBe(24); + expect(catalog.vmInventoryExport.exportEndpoint).toBe( + '/api/admin/reports/inventory/vms/export', + ); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/reportingInventoryExportModel.test.ts b/frontend-modern/src/components/Settings/__tests__/reportingInventoryExportModel.test.ts index 8923a6d0a..8b642ff9e 100644 --- a/frontend-modern/src/components/Settings/__tests__/reportingInventoryExportModel.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/reportingInventoryExportModel.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from 'vitest'; import { - buildVMInventoryExportDefinitionRequest, buildVMInventoryExportFilename, buildVMInventoryExportRequest, parseVMInventoryExportDefinition, @@ -14,24 +13,22 @@ describe('reporting inventory export model', () => { it('builds the canonical VM inventory export request', () => { const now = new Date('2026-03-20T12:34:56.000Z'); - const request = buildVMInventoryExportRequest(now, { filenamePrefix: 'vm-inventory' }); + const request = buildVMInventoryExportRequest(now, { + exportEndpoint: '/api/admin/reports/inventory/vms/export', + filenamePrefix: 'vm-inventory', + }); expect(request.filename).toBe('vm-inventory-2026-03-20.csv'); expect(request.request.url).toBe('/api/admin/reports/inventory/vms/export?format=csv'); }); - it('builds the canonical VM inventory definition request', () => { - expect(buildVMInventoryExportDefinitionRequest()).toEqual({ - url: '/api/admin/reports/inventory/vms/definition', - }); - }); - it('parses the canonical VM inventory export definition payload', () => { const definition = parseVMInventoryExportDefinition({ id: 'vm_inventory', title: 'VM Inventory Export', description: 'Current-state VM inventory', format: 'csv', + exportEndpoint: '/api/admin/reports/inventory/vms/export', filenamePrefix: 'vm-inventory', columns: [ { @@ -43,6 +40,7 @@ describe('reporting inventory export model', () => { }); expect(definition.id).toBe('vm_inventory'); + expect(definition.exportEndpoint).toBe('/api/admin/reports/inventory/vms/export'); expect(definition.columns[0]).toEqual({ key: 'pool', label: 'Pool', diff --git a/frontend-modern/src/components/Settings/__tests__/reportingPanelModel.test.ts b/frontend-modern/src/components/Settings/__tests__/reportingPanelModel.test.ts index f84e3fa19..ac8e62930 100644 --- a/frontend-modern/src/components/Settings/__tests__/reportingPanelModel.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/reportingPanelModel.test.ts @@ -4,6 +4,46 @@ import { buildReportingRequest, getReportingRangeStart, } from '../reportingPanelModel'; +import type { ReportingPerformanceReportDefinition } from '../reportingCatalogModel'; + +const performanceDefinition: ReportingPerformanceReportDefinition = { + id: 'performance_reports', + title: 'Performance Reports', + description: 'Historical performance reporting', + singleResourceEndpoint: '/api/admin/reports/generate', + multiResourceEndpoint: '/api/admin/reports/generate-multi', + singleFilenamePrefix: 'report', + multiFilenamePrefix: 'fleet-report', + formats: [ + { value: 'pdf', label: 'PDF Report' }, + { value: 'csv', label: 'CSV Data' }, + ], + defaultFormat: 'pdf', + ranges: [ + { + key: '24h', + label: 'Last 24 Hours', + description: 'Daily review', + windowHours: 24, + }, + { + key: '7d', + label: 'Last 7 Days', + description: 'Weekly review', + windowHours: 168, + }, + { + key: '30d', + label: 'Last 30 Days', + description: 'Monthly review', + windowHours: 720, + }, + ], + defaultRange: '24h', + multiResourceMax: 50, + supportsMetricFilter: true, + supportsCustomTitle: true, +}; describe('reporting panel model', () => { it('builds a single-resource reporting request and filename', () => { @@ -24,7 +64,7 @@ describe('reporting panel model', () => { resources, start: '2026-03-19T12:34:56.000Z', title: '', - }); + }, performanceDefinition); expect(request.filename).toBe('report-node-a-2026-03-20.pdf'); expect(request.request.init).toBeUndefined(); @@ -58,7 +98,7 @@ describe('reporting panel model', () => { resources, start: '2026-03-19T12:34:56.000Z', title: '', - }); + }, performanceDefinition); expect(request.filename).toBe('fleet-report-2026-03-20.csv'); expect(request.request.url).toBe('/api/admin/reports/generate-multi'); @@ -84,8 +124,14 @@ describe('reporting panel model', () => { it('derives canonical range starts from the selected preset', () => { const now = new Date('2026-03-20T12:00:00.000Z'); - expect(getReportingRangeStart('24h', now).toISOString()).toBe('2026-03-19T12:00:00.000Z'); - expect(getReportingRangeStart('7d', now).toISOString()).toBe('2026-03-13T12:00:00.000Z'); - expect(getReportingRangeStart('30d', now).toISOString()).toBe('2026-02-18T12:00:00.000Z'); + expect(getReportingRangeStart('24h', now, performanceDefinition).toISOString()).toBe( + '2026-03-19T12:00:00.000Z', + ); + expect(getReportingRangeStart('7d', now, performanceDefinition).toISOString()).toBe( + '2026-03-13T12:00:00.000Z', + ); + expect(getReportingRangeStart('30d', now, performanceDefinition).toISOString()).toBe( + '2026-02-18T12:00:00.000Z', + ); }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index e378fa47a..ce18bc802 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -126,6 +126,7 @@ import ssoProviderPresentationSource from '@/utils/ssoProviderPresentation.ts?ra import systemSettingsPresentationSource from '@/utils/systemSettingsPresentation.ts?raw'; import updatesPresentationSource from '@/utils/updatesPresentation.ts?raw'; import diagnosticsStateSource from '../useDiagnosticsPanelState.ts?raw'; +import reportingCatalogModelSource from '../reportingCatalogModel.ts?raw'; import reportingPanelModelSource from '../reportingPanelModel.ts?raw'; import reportingInventoryExportModelSource from '../reportingInventoryExportModel.ts?raw'; import reportingPanelSource from '../ReportingPanel.tsx?raw'; @@ -1082,8 +1083,9 @@ describe('Settings architecture guardrails', () => { it('keeps the reporting shell behind extracted runtime and model owners', () => { expect(reportingPanelSource).toContain('@/components/Settings/OperationsPanel'); expect(reportingPanelSource).toContain('@/components/Settings/useReportingPanelState'); + expect(reportingPanelSource).toContain('@/components/Settings/reportingCatalogModel'); expect(reportingPanelSource).toContain('@/components/Settings/reportingPanelModel'); - expect(reportingPanelSource).toContain('VM Inventory Export'); + expect(reportingPanelSource).toContain('reportingCatalog'); expect(reportingPanelSource).not.toContain('loadLicenseStatus()'); expect(reportingPanelSource).not.toContain('startProTrial()'); expect(reportingPanelSource).not.toContain("apiFetch('/api/admin/reports/generate"); @@ -1093,19 +1095,19 @@ describe('Settings architecture guardrails', () => { expect(reportingPanelStateSource).toContain('runStartProTrialAction({'); expect(reportingPanelStateSource).not.toContain('startProTrial()'); expect(reportingPanelStateSource).toContain('buildReportingRequest'); - expect(reportingPanelStateSource).toContain('buildVMInventoryExportDefinitionRequest'); + expect(reportingPanelStateSource).toContain('buildReportingCatalogRequest'); + expect(reportingPanelStateSource).toContain('parseReportingCatalog'); expect(reportingPanelStateSource).toContain('buildVMInventoryExportRequest'); expect(reportingPanelStateSource).toContain('getReportingGenerateSuccessMessage'); expect(reportingPanelStateSource).not.toContain('getTrialAlreadyUsedMessage()'); + expect(reportingCatalogModelSource).toContain('export function buildReportingCatalogRequest'); + expect(reportingCatalogModelSource).toContain('export function parseReportingCatalog'); expect(reportingPanelModelSource).toContain('export function getReportingRangeStart'); expect(reportingPanelModelSource).toContain('export function buildReportingRequest'); expect(reportingPanelModelSource).toContain('export function buildReportingFilename'); expect(reportingInventoryExportModelSource).toContain( 'export function buildVMInventoryExportFilename', ); - expect(reportingInventoryExportModelSource).toContain( - 'export function buildVMInventoryExportDefinitionRequest', - ); expect(reportingInventoryExportModelSource).toContain( 'export function parseVMInventoryExportDefinition', ); diff --git a/frontend-modern/src/components/Settings/reportingCatalogModel.ts b/frontend-modern/src/components/Settings/reportingCatalogModel.ts new file mode 100644 index 000000000..46ae49130 --- /dev/null +++ b/frontend-modern/src/components/Settings/reportingCatalogModel.ts @@ -0,0 +1,157 @@ +import { + parseVMInventoryExportDefinition, + type ReportingInventoryExportDefinition, +} from '@/components/Settings/reportingInventoryExportModel'; + +export type ReportingFormat = 'pdf' | 'csv'; + +export interface ReportingFormatDefinition { + value: ReportingFormat; + label: string; +} + +export interface ReportingRangeDefinition { + key: string; + label: string; + description: string; + windowHours: number; +} + +export interface ReportingPerformanceReportDefinition { + id: string; + title: string; + description: string; + singleResourceEndpoint: string; + multiResourceEndpoint: string; + singleFilenamePrefix: string; + multiFilenamePrefix: string; + formats: ReportingFormatDefinition[]; + defaultFormat: ReportingFormat; + ranges: ReportingRangeDefinition[]; + defaultRange: string; + multiResourceMax: number; + supportsMetricFilter: boolean; + supportsCustomTitle: boolean; +} + +export interface ReportingCatalog { + id: string; + title: string; + description: string; + performanceReport: ReportingPerformanceReportDefinition; + vmInventoryExport: ReportingInventoryExportDefinition; +} + +export function buildReportingCatalogRequest(): { url: string } { + return { + url: '/api/admin/reports/catalog', + }; +} + +function parseReportingFormatDefinition(input: unknown): ReportingFormatDefinition { + if (!input || typeof input !== 'object') { + throw new Error('Invalid reporting catalog payload'); + } + const candidate = input as Partial; + if ( + (candidate.value !== 'pdf' && candidate.value !== 'csv') || + typeof candidate.label !== 'string' + ) { + throw new Error('Invalid reporting catalog payload'); + } + return { + value: candidate.value, + label: candidate.label, + }; +} + +function parseReportingRangeDefinition(input: unknown): ReportingRangeDefinition { + if (!input || typeof input !== 'object') { + throw new Error('Invalid reporting catalog payload'); + } + const candidate = input as Partial; + if ( + typeof candidate.key !== 'string' || + typeof candidate.label !== 'string' || + typeof candidate.description !== 'string' || + typeof candidate.windowHours !== 'number' || + !Number.isFinite(candidate.windowHours) || + candidate.windowHours <= 0 + ) { + throw new Error('Invalid reporting catalog payload'); + } + return { + key: candidate.key, + label: candidate.label, + description: candidate.description, + windowHours: candidate.windowHours, + }; +} + +function parseReportingPerformanceReportDefinition( + input: unknown, +): ReportingPerformanceReportDefinition { + if (!input || typeof input !== 'object') { + throw new Error('Invalid reporting catalog payload'); + } + const candidate = input as Partial; + if ( + typeof candidate.id !== 'string' || + typeof candidate.title !== 'string' || + typeof candidate.description !== 'string' || + typeof candidate.singleResourceEndpoint !== 'string' || + typeof candidate.multiResourceEndpoint !== 'string' || + typeof candidate.singleFilenamePrefix !== 'string' || + typeof candidate.multiFilenamePrefix !== 'string' || + !Array.isArray(candidate.formats) || + (candidate.defaultFormat !== 'pdf' && candidate.defaultFormat !== 'csv') || + !Array.isArray(candidate.ranges) || + typeof candidate.defaultRange !== 'string' || + typeof candidate.multiResourceMax !== 'number' || + !Number.isFinite(candidate.multiResourceMax) || + candidate.multiResourceMax <= 0 || + typeof candidate.supportsMetricFilter !== 'boolean' || + typeof candidate.supportsCustomTitle !== 'boolean' + ) { + throw new Error('Invalid reporting catalog payload'); + } + + return { + id: candidate.id, + title: candidate.title, + description: candidate.description, + singleResourceEndpoint: candidate.singleResourceEndpoint, + multiResourceEndpoint: candidate.multiResourceEndpoint, + singleFilenamePrefix: candidate.singleFilenamePrefix, + multiFilenamePrefix: candidate.multiFilenamePrefix, + formats: candidate.formats.map(parseReportingFormatDefinition), + defaultFormat: candidate.defaultFormat, + ranges: candidate.ranges.map(parseReportingRangeDefinition), + defaultRange: candidate.defaultRange, + multiResourceMax: candidate.multiResourceMax, + supportsMetricFilter: candidate.supportsMetricFilter, + supportsCustomTitle: candidate.supportsCustomTitle, + }; +} + +export function parseReportingCatalog(input: unknown): ReportingCatalog { + if (!input || typeof input !== 'object') { + throw new Error('Invalid reporting catalog payload'); + } + const candidate = input as Partial; + if ( + typeof candidate.id !== 'string' || + typeof candidate.title !== 'string' || + typeof candidate.description !== 'string' + ) { + throw new Error('Invalid reporting catalog payload'); + } + + return { + id: candidate.id, + title: candidate.title, + description: candidate.description, + performanceReport: parseReportingPerformanceReportDefinition(candidate.performanceReport), + vmInventoryExport: parseVMInventoryExportDefinition(candidate.vmInventoryExport), + }; +} diff --git a/frontend-modern/src/components/Settings/reportingInventoryExportModel.ts b/frontend-modern/src/components/Settings/reportingInventoryExportModel.ts index dd56469a0..a8ae68746 100644 --- a/frontend-modern/src/components/Settings/reportingInventoryExportModel.ts +++ b/frontend-modern/src/components/Settings/reportingInventoryExportModel.ts @@ -16,6 +16,7 @@ export interface ReportingInventoryExportDefinition { title: string; description: string; format: 'csv'; + exportEndpoint: string; filenamePrefix: string; columns: ReportingInventoryExportColumnDefinition[]; } @@ -25,21 +26,15 @@ export function buildVMInventoryExportFilename(now: Date, filenamePrefix = 'vm-i return `${filenamePrefix}-${date}.csv`; } -export function buildVMInventoryExportDefinitionRequest(): { url: string } { - return { - url: '/api/admin/reports/inventory/vms/definition', - }; -} - export function buildVMInventoryExportRequest( now: Date, - definition?: Pick | null, + definition?: Pick | null, ): ReportingInventoryExportRequestDefinition { const params = new URLSearchParams({ format: 'csv' }); return { filename: buildVMInventoryExportFilename(now, definition?.filenamePrefix ?? 'vm-inventory'), request: { - url: `/api/admin/reports/inventory/vms/export?${params.toString()}`, + url: `${definition?.exportEndpoint ?? '/api/admin/reports/inventory/vms/export'}?${params.toString()}`, }, }; } @@ -57,6 +52,7 @@ export function parseVMInventoryExportDefinition( typeof candidate.title !== 'string' || typeof candidate.description !== 'string' || candidate.format !== 'csv' || + typeof candidate.exportEndpoint !== 'string' || typeof candidate.filenamePrefix !== 'string' || !Array.isArray(candidate.columns) ) { @@ -86,6 +82,7 @@ export function parseVMInventoryExportDefinition( title: candidate.title, description: candidate.description, format: 'csv', + exportEndpoint: candidate.exportEndpoint, filenamePrefix: candidate.filenamePrefix, columns, }; diff --git a/frontend-modern/src/components/Settings/reportingPanelModel.ts b/frontend-modern/src/components/Settings/reportingPanelModel.ts index d0ab608ae..ba1cdc5de 100644 --- a/frontend-modern/src/components/Settings/reportingPanelModel.ts +++ b/frontend-modern/src/components/Settings/reportingPanelModel.ts @@ -1,9 +1,11 @@ -import type { ReportingRangeOption } from '@/utils/reportingPresentation'; import { toReportingResourceType } from '@/utils/reportingResourceTypes'; import type { SelectedResource } from '@/components/Settings/ResourcePicker'; +import type { + ReportingFormat, + ReportingPerformanceReportDefinition, +} from '@/components/Settings/reportingCatalogModel'; -export type ReportingRangeValue = ReportingRangeOption['value']; -export type ReportingFormat = 'pdf' | 'csv'; +export type ReportingRangeValue = string; export interface ReportingRequestContext { end: string; @@ -27,11 +29,17 @@ export interface ReportingRequestDefinition { }; } -export function getReportingRangeStart(range: ReportingRangeValue, now: Date): Date { +export function getReportingRangeStart( + range: ReportingRangeValue, + now: Date, + definition?: Pick | null, +): Date { const start = new Date(now); - if (range === '24h') start.setHours(start.getHours() - 24); - else if (range === '7d') start.setDate(start.getDate() - 7); - else if (range === '30d') start.setDate(start.getDate() - 30); + const resolvedRange = + definition?.ranges.find((candidate) => candidate.key === range) ?? + definition?.ranges.find((candidate) => candidate.key === definition.defaultRange) ?? + null; + start.setHours(start.getHours() - (resolvedRange?.windowHours ?? 24)); return start; } @@ -47,15 +55,25 @@ export function buildReportingFilename( format: ReportingFormat, resourceName: string | null, now: Date, + definition?: Pick< + ReportingPerformanceReportDefinition, + 'multiFilenamePrefix' | 'singleFilenamePrefix' + > | null, ): string { const date = now.toISOString().split('T')[0]; if (resourceName) { - return `report-${resourceName}-${date}.${format}`; + return `${definition?.singleFilenamePrefix ?? 'report'}-${resourceName}-${date}.${format}`; } - return `fleet-report-${date}.${format}`; + return `${definition?.multiFilenamePrefix ?? 'fleet-report'}-${date}.${format}`; } -export function buildReportingRequest(context: ReportingRequestContext): ReportingRequestDefinition { +export function buildReportingRequest( + context: ReportingRequestContext, + definition?: Pick< + ReportingPerformanceReportDefinition, + 'multiFilenamePrefix' | 'multiResourceEndpoint' | 'singleFilenamePrefix' | 'singleResourceEndpoint' + > | null, +): ReportingRequestDefinition { if (context.resources.length === 1) { const resource = context.resources[0]; const params = new URLSearchParams({ @@ -72,17 +90,17 @@ export function buildReportingRequest(context: ReportingRequestContext): Reporti } return { - filename: buildReportingFilename(context.format, resource.name, context.now), + filename: buildReportingFilename(context.format, resource.name, context.now, definition), request: { - url: `/api/admin/reports/generate?${params.toString()}`, + url: `${definition?.singleResourceEndpoint ?? '/api/admin/reports/generate'}?${params.toString()}`, }, }; } return { - filename: buildReportingFilename(context.format, null, context.now), + filename: buildReportingFilename(context.format, null, context.now, definition), request: { - url: '/api/admin/reports/generate-multi', + url: definition?.multiResourceEndpoint ?? '/api/admin/reports/generate-multi', init: { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/frontend-modern/src/components/Settings/useReportingPanelState.ts b/frontend-modern/src/components/Settings/useReportingPanelState.ts index e75e98e31..215ae9e8f 100644 --- a/frontend-modern/src/components/Settings/useReportingPanelState.ts +++ b/frontend-modern/src/components/Settings/useReportingPanelState.ts @@ -11,6 +11,7 @@ import { } from '@/stores/license'; import { trackPaywallViewed } from '@/utils/upgradeMetrics'; import { + getReportingCatalogErrorMessage, getReportingGenerateErrorMessage, getReportingGenerateSelectionRequiredMessage, getReportingGenerateSuccessMessage, @@ -21,15 +22,17 @@ import { runStartProTrialAction } from '@/utils/trialStartAction'; import { buildReportingRequest, getReportingRangeStart, - type ReportingFormat, type ReportingRangeValue, } from '@/components/Settings/reportingPanelModel'; import { - buildVMInventoryExportDefinitionRequest, buildVMInventoryExportRequest, - parseVMInventoryExportDefinition, - type ReportingInventoryExportDefinition, } from '@/components/Settings/reportingInventoryExportModel'; +import { + buildReportingCatalogRequest, + parseReportingCatalog, + type ReportingCatalog, + type ReportingFormat, +} from '@/components/Settings/reportingCatalogModel'; export const useReportingPanelState = () => { const [selectedResources, setSelectedResources] = createSignal([]); @@ -38,11 +41,10 @@ export const useReportingPanelState = () => { const [range, setRange] = createSignal('24h'); const [generating, setGenerating] = createSignal(false); const [exportingInventory, setExportingInventory] = createSignal(false); - const [inventoryDefinition, setInventoryDefinition] = - createSignal(null); - const [inventoryDefinitionLoading, setInventoryDefinitionLoading] = createSignal(false); - const [inventoryDefinitionError, setInventoryDefinitionError] = createSignal(''); - const [inventoryDefinitionRequested, setInventoryDefinitionRequested] = createSignal(false); + const [reportingCatalog, setReportingCatalog] = createSignal(null); + const [reportingCatalogLoading, setReportingCatalogLoading] = createSignal(false); + const [reportingCatalogError, setReportingCatalogError] = createSignal(''); + const [reportingCatalogRequested, setReportingCatalogRequested] = createSignal(false); const [title, setTitle] = createSignal(''); const [startingTrial, setStartingTrial] = createSignal(false); @@ -66,37 +68,50 @@ export const useReportingPanelState = () => { createEffect(() => { if ( !isReportingEnabled() || - inventoryDefinition() || - inventoryDefinitionLoading() || - inventoryDefinitionRequested() + reportingCatalog() || + reportingCatalogLoading() || + reportingCatalogRequested() ) { return; } void (async () => { - setInventoryDefinitionRequested(true); - setInventoryDefinitionLoading(true); - setInventoryDefinitionError(''); + setReportingCatalogRequested(true); + setReportingCatalogLoading(true); + setReportingCatalogError(''); try { - const request = buildVMInventoryExportDefinitionRequest(); + const request = buildReportingCatalogRequest(); const response = await apiFetch(request.url); if (!response.ok) { const text = await response.text(); - throw new Error(text || getReportingInventoryExportErrorMessage()); + throw new Error(text || getReportingCatalogErrorMessage()); } - setInventoryDefinition(parseVMInventoryExportDefinition(await response.json())); + setReportingCatalog(parseReportingCatalog(await response.json())); } catch (error) { - console.error('VM inventory export definition error:', error); - setInventoryDefinitionError( - error instanceof Error ? error.message : getReportingInventoryExportErrorMessage(), + console.error('Reporting catalog error:', error); + setReportingCatalogError( + error instanceof Error ? error.message : getReportingCatalogErrorMessage(), ); } finally { - setInventoryDefinitionLoading(false); + setReportingCatalogLoading(false); } })(); }); + createEffect(() => { + const performanceReport = reportingCatalog()?.performanceReport; + if (!performanceReport) { + return; + } + if (!performanceReport.formats.some((candidate) => candidate.value === format())) { + setFormat(performanceReport.defaultFormat); + } + if (!performanceReport.ranges.some((candidate) => candidate.key === range())) { + setRange(performanceReport.defaultRange); + } + }); + const handleStartTrial = async () => { if (startingTrial()) return; setStartingTrial(true); @@ -132,7 +147,11 @@ export const useReportingPanelState = () => { setGenerating(true); try { const now = new Date(); - const start = getReportingRangeStart(range(), now); + const performanceReport = reportingCatalog()?.performanceReport; + if (!performanceReport) { + throw new Error(getReportingGenerateErrorMessage()); + } + const start = getReportingRangeStart(range(), now, performanceReport); const request = buildReportingRequest({ end: now.toISOString(), format: format(), @@ -141,7 +160,7 @@ export const useReportingPanelState = () => { resources, start: start.toISOString(), title: title(), - }); + }, performanceReport); const response = await apiFetch(request.request.url, request.request.init); if (!response.ok) { @@ -165,7 +184,11 @@ export const useReportingPanelState = () => { setExportingInventory(true); try { - const request = buildVMInventoryExportRequest(new Date(), inventoryDefinition()); + const inventoryDefinition = reportingCatalog()?.vmInventoryExport; + if (!inventoryDefinition) { + throw new Error(getReportingInventoryExportErrorMessage()); + } + const request = buildVMInventoryExportRequest(new Date(), inventoryDefinition); const response = await apiFetch(request.request.url); if (!response.ok) { const text = await response.text(); @@ -192,13 +215,13 @@ export const useReportingPanelState = () => { generating, handleGenerate, handleStartTrial, - inventoryDefinition, - inventoryDefinitionError, - inventoryDefinitionLoading, isLocked, isReportingEnabled, metricType, range, + reportingCatalog, + reportingCatalogError, + reportingCatalogLoading, selectedResources, setFormat, setMetricType, diff --git a/frontend-modern/src/utils/__tests__/reportingPresentation.test.ts b/frontend-modern/src/utils/__tests__/reportingPresentation.test.ts index f4e32d854..e4ec79dd6 100644 --- a/frontend-modern/src/utils/__tests__/reportingPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/reportingPresentation.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; import { + getReportingCatalogErrorMessage, getReportingGenerateErrorMessage, getReportingGenerateSelectionRequiredMessage, getReportingGenerateSuccessMessage, @@ -19,6 +20,7 @@ describe('reportingPresentation', () => { expect(getReportingGenerateSelectionRequiredMessage()).toBe( 'Please select at least one resource', ); + expect(getReportingCatalogErrorMessage()).toBe('Failed to load reporting surfaces'); expect(getReportingGenerateSuccessMessage()).toBe('Report generated successfully'); expect(getReportingGenerateErrorMessage()).toBe('Failed to generate report'); }); diff --git a/frontend-modern/src/utils/reportingPresentation.ts b/frontend-modern/src/utils/reportingPresentation.ts index 84b9f9795..9cc52d5b7 100644 --- a/frontend-modern/src/utils/reportingPresentation.ts +++ b/frontend-modern/src/utils/reportingPresentation.ts @@ -21,6 +21,10 @@ export function getReportingGenerateErrorMessage(): string { return 'Failed to generate report'; } +export function getReportingCatalogErrorMessage(): string { + return 'Failed to load reporting surfaces'; +} + export function getReportingInventoryExportSuccessMessage(): string { return 'VM inventory export generated successfully'; } diff --git a/internal/api/audit_reporting_scope_test.go b/internal/api/audit_reporting_scope_test.go index f00c8cf60..57db3a7f1 100644 --- a/internal/api/audit_reporting_scope_test.go +++ b/internal/api/audit_reporting_scope_test.go @@ -47,6 +47,7 @@ func TestReportingEndpointsRequireSettingsReadScope(t *testing.T) { router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") paths := []string{ + "/api/admin/reports/catalog", "/api/admin/reports/generate", "/api/admin/reports/generate-multi", "/api/admin/reports/inventory/vms/definition", diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index 08a5bc64e..67de48c3c 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -329,6 +329,143 @@ func TestContract_VMInventoryExportCSVHeaders(t *testing.T) { } } +func TestContract_ReportingCatalogJSONSnapshot(t *testing.T) { + handler := NewReportingHandlers(nil, nil) + req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil) + rec := httptest.NewRecorder() + + handler.HandleGetReportingCatalog(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String()) + } + if got := rec.Header().Get("Content-Type"); got != "application/json" { + t.Fatalf("expected json content type, got %q", got) + } + + const want = `{ + "id":"advanced_reporting", + "title":"Detailed Reporting", + "description":"Generate performance reports and current-state exports across infrastructure and workloads.", + "performanceReport":{ + "id":"performance_reports", + "title":"Performance Reports", + "description":"Generate PDF summaries or CSV metric exports from historical monitoring data for one or more selected resources.", + "singleResourceEndpoint":"/api/admin/reports/generate", + "multiResourceEndpoint":"/api/admin/reports/generate-multi", + "singleFilenamePrefix":"report", + "multiFilenamePrefix":"fleet-report", + "formats":[ + { + "value":"pdf", + "label":"PDF Report" + }, + { + "value":"csv", + "label":"CSV Data" + } + ], + "defaultFormat":"pdf", + "ranges":[ + { + "key":"24h", + "label":"Last 24 Hours", + "description":"Current-day operational summary for short-term regressions.", + "windowHours":24 + }, + { + "key":"7d", + "label":"Last 7 Days", + "description":"Weekly trend window for recent performance changes.", + "windowHours":168 + }, + { + "key":"30d", + "label":"Last 30 Days", + "description":"Monthly review window for sustained capacity or reliability shifts.", + "windowHours":720 + } + ], + "defaultRange":"24h", + "multiResourceMax":50, + "supportsMetricFilter":true, + "supportsCustomTitle":true + }, + "vmInventoryExport":{ + "id":"vm_inventory", + "title":"VM Inventory Export", + "description":"Export the current fleet-wide VM inventory as CSV using the canonical runtime model. Includes VM identity, placement, CPU, memory allocation, disk allocation, and disk usage columns.", + "format":"csv", + "exportEndpoint":"/api/admin/reports/inventory/vms/export", + "filenamePrefix":"vm-inventory", + "columns":[ + { + "key":"resource_id", + "label":"Resource ID", + "description":"Canonical Pulse resource ID for the VM." + }, + { + "key":"instance", + "label":"Instance", + "description":"Configured Proxmox instance or cluster name." + }, + { + "key":"node", + "label":"Node", + "description":"Proxmox node currently hosting the VM." + }, + { + "key":"pool", + "label":"Pool", + "description":"Canonical Proxmox pool membership when the platform reports one." + }, + { + "key":"vmid", + "label":"VMID", + "description":"Numeric Proxmox VM identifier." + }, + { + "key":"vm_name", + "label":"VM Name", + "description":"Current VM display name from the runtime model." + }, + { + "key":"status", + "label":"Status", + "description":"Canonical runtime status for the VM." + }, + { + "key":"cpu_cores", + "label":"CPU Cores", + "description":"Allocated virtual CPU core count." + }, + { + "key":"memory_allocated_bytes", + "label":"Memory Allocated Bytes", + "description":"Configured memory allocation in bytes." + }, + { + "key":"disk_allocated_bytes", + "label":"Disk Allocated Bytes", + "description":"Total allocated disk capacity in bytes across the VM." + }, + { + "key":"disk_used_bytes", + "label":"Disk Used Bytes", + "description":"Current used disk bytes from the canonical runtime disk view." + }, + { + "key":"disk_status_reason", + "label":"Disk Status Reason", + "description":"Reason disk usage is partial or unavailable when the runtime cannot provide a full guest view." + } + ] + } + }` + + assertJSONSnapshot(t, rec.Body.Bytes(), want) +} + func TestContract_VMInventoryExportDefinitionJSONSnapshot(t *testing.T) { handler := NewReportingHandlers(nil, nil) req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/inventory/vms/definition", nil) @@ -348,6 +485,7 @@ func TestContract_VMInventoryExportDefinitionJSONSnapshot(t *testing.T) { "title":"VM Inventory Export", "description":"Export the current fleet-wide VM inventory as CSV using the canonical runtime model. Includes VM identity, placement, CPU, memory allocation, disk allocation, and disk usage columns.", "format":"csv", + "exportEndpoint":"/api/admin/reports/inventory/vms/export", "filenamePrefix":"vm-inventory", "columns":[ { diff --git a/internal/api/enterprise_extension_reporting_admin_test.go b/internal/api/enterprise_extension_reporting_admin_test.go index fbd8460ae..6a0ef7f11 100644 --- a/internal/api/enterprise_extension_reporting_admin_test.go +++ b/internal/api/enterprise_extension_reporting_admin_test.go @@ -9,11 +9,16 @@ import ( ) type testReportingAdminEndpoints struct { + catalogCalls int generateCalls int definitionCalls int exportInventoryCalls int } +func (t *testReportingAdminEndpoints) HandleGetReportingCatalog(http.ResponseWriter, *http.Request) { + t.catalogCalls++ +} + func (t *testReportingAdminEndpoints) HandleGenerateReport(http.ResponseWriter, *http.Request) { t.generateCalls++ } @@ -85,6 +90,22 @@ func TestResolveReportingAdminEndpoints_UsesDefaultInventoryHandler(t *testing.T } } +func TestResolveReportingAdminEndpoints_UsesDefaultCatalogHandler(t *testing.T) { + SetReportingAdminEndpointsBinder(nil) + t.Cleanup(func() { + SetReportingAdminEndpointsBinder(nil) + }) + + defaults := &testReportingAdminEndpoints{} + resolved := resolveReportingAdminEndpoints(defaults, extensions.ReportingAdminRuntime{}) + req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil) + rec := httptest.NewRecorder() + resolved.HandleGetReportingCatalog(rec, req) + if defaults.catalogCalls != 1 { + t.Fatalf("expected default reporting catalog handler call, got %d", defaults.catalogCalls) + } +} + func TestResolveReportingAdminEndpoints_UsesDefaultInventoryDefinitionHandler(t *testing.T) { SetReportingAdminEndpointsBinder(nil) t.Cleanup(func() { diff --git a/internal/api/rbac_reporting_auth_test.go b/internal/api/rbac_reporting_auth_test.go index 6a8461169..1ca1e215a 100644 --- a/internal/api/rbac_reporting_auth_test.go +++ b/internal/api/rbac_reporting_auth_test.go @@ -37,6 +37,7 @@ func TestReportingEndpointsRequireAuthInAPIMode(t *testing.T) { path string body string }{ + {method: http.MethodGet, path: "/api/admin/reports/catalog", body: ""}, {method: http.MethodGet, path: "/api/admin/reports/generate", body: ""}, {method: http.MethodPost, path: "/api/admin/reports/generate-multi", body: `{}`}, {method: http.MethodGet, path: "/api/admin/reports/inventory/vms/definition", body: ""}, diff --git a/internal/api/reporting_catalog_handlers.go b/internal/api/reporting_catalog_handlers.go new file mode 100644 index 000000000..b3ba608f7 --- /dev/null +++ b/internal/api/reporting_catalog_handlers.go @@ -0,0 +1,20 @@ +package api + +import ( + "encoding/json" + "net/http" + + "github.com/rcourtman/pulse-go-rewrite/pkg/reporting" +) + +// HandleGetReportingCatalog returns the canonical operator-facing reporting +// catalog for the admin settings surface. +func (h *ReportingHandlers) HandleGetReportingCatalog(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(reporting.DescribeReportingCatalog()) +} diff --git a/internal/api/reporting_handlers_test.go b/internal/api/reporting_handlers_test.go index a98f7e036..c3286f1fa 100644 --- a/internal/api/reporting_handlers_test.go +++ b/internal/api/reporting_handlers_test.go @@ -237,6 +237,47 @@ func TestReportingHandlers_ExportVMInventory_MethodNotAllowed(t *testing.T) { } } +func TestReportingHandlers_GetReportingCatalog_MethodNotAllowed(t *testing.T) { + handler := NewReportingHandlers(nil, nil) + req := httptest.NewRequest(http.MethodPost, "/api/admin/reports/catalog", nil) + rr := httptest.NewRecorder() + + handler.HandleGetReportingCatalog(rr, req) + + if rr.Code != http.StatusMethodNotAllowed { + t.Fatalf("expected status %d, got %d", http.StatusMethodNotAllowed, rr.Code) + } +} + +func TestReportingHandlers_GetReportingCatalog_ReturnsCanonicalDefinition(t *testing.T) { + handler := NewReportingHandlers(nil, nil) + req := httptest.NewRequest(http.MethodGet, "/api/admin/reports/catalog", nil) + rr := httptest.NewRecorder() + + handler.HandleGetReportingCatalog(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code) + } + if got := rr.Header().Get("Content-Type"); !strings.Contains(got, "application/json") { + t.Fatalf("expected JSON content-type, got %q", got) + } + + var payload reporting.ReportingCatalog + if err := json.NewDecoder(rr.Body).Decode(&payload); err != nil { + t.Fatalf("decode catalog response: %v", err) + } + if payload.ID != "advanced_reporting" { + t.Fatalf("expected advanced_reporting id, got %q", payload.ID) + } + if payload.PerformanceReport.ID != "performance_reports" { + t.Fatalf("expected performance report definition, got %#v", payload.PerformanceReport) + } + if payload.VMInventoryExport.ID != "vm_inventory" { + t.Fatalf("expected vm inventory definition, got %#v", payload.VMInventoryExport) + } +} + func TestReportingHandlers_GetVMInventoryDefinition_MethodNotAllowed(t *testing.T) { handler := NewReportingHandlers(nil, nil) req := httptest.NewRequest(http.MethodPost, "/api/admin/reports/inventory/vms/definition", nil) diff --git a/internal/api/route_inventory_test.go b/internal/api/route_inventory_test.go index 741d75bba..14ebe3cf4 100644 --- a/internal/api/route_inventory_test.go +++ b/internal/api/route_inventory_test.go @@ -449,6 +449,7 @@ var allRouteAllowlist = []string{ "POST /api/admin/rbac/reset-admin", "/api/admin/reports/generate", "/api/admin/reports/generate-multi", + "/api/admin/reports/catalog", "/api/admin/reports/inventory/vms/definition", "/api/admin/reports/inventory/vms/export", "/api/admin/webhooks/audit", diff --git a/internal/api/router_routes_licensing.go b/internal/api/router_routes_licensing.go index 68b08616c..a378dbea1 100644 --- a/internal/api/router_routes_licensing.go +++ b/internal/api/router_routes_licensing.go @@ -177,6 +177,12 @@ func (r *Router) registerOrgLicenseRoutesGroup(orgHandlers *OrgHandlers, rbacHan })) // Advanced Reporting routes + r.mux.HandleFunc("/api/admin/reports/catalog", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceNodes, func(w http.ResponseWriter, req *http.Request) { + if !ensureAdminSession(r.config, w, req) { + return + } + RequireLicenseFeature(r.licenseHandlers, featureAdvancedReportingValue, RequireScope(config.ScopeSettingsRead, reportingAdminEndpoints.HandleGetReportingCatalog))(w, req) + })) r.mux.HandleFunc("/api/admin/reports/generate", RequirePermission(r.config, r.authorizer, auth.ActionRead, auth.ResourceNodes, func(w http.ResponseWriter, req *http.Request) { if !ensureAdminSession(r.config, w, req) { return @@ -299,6 +305,14 @@ type reportingAdminEndpointAdapter struct { var _ extensions.ReportingAdminEndpoints = reportingAdminEndpointAdapter{} +func (a reportingAdminEndpointAdapter) HandleGetReportingCatalog(w http.ResponseWriter, req *http.Request) { + if a.handlers == nil { + writeErrorResponse(w, http.StatusNotImplemented, "reporting_unavailable", "Reporting is not available", nil) + return + } + a.handlers.HandleGetReportingCatalog(w, req) +} + func (a reportingAdminEndpointAdapter) HandleGenerateReport(w http.ResponseWriter, req *http.Request) { if a.handlers == nil { writeErrorResponse(w, http.StatusNotImplemented, "reporting_unavailable", "Reporting is not available", nil) diff --git a/internal/api/security_regression_test.go b/internal/api/security_regression_test.go index 01ce44b1d..9fbf57cf6 100644 --- a/internal/api/security_regression_test.go +++ b/internal/api/security_regression_test.go @@ -2900,6 +2900,7 @@ func TestReportingEndpointsRequireLicenseFeature(t *testing.T) { router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0") paths := []string{ + "/api/admin/reports/catalog", "/api/admin/reports/generate", "/api/admin/reports/generate-multi", } @@ -3156,6 +3157,7 @@ func TestPermissionProtectedEndpointsDenyWhenAuthorizerBlocks(t *testing.T) { {method: http.MethodPut, path: "/api/admin/users/alice/roles", body: `{"roleIds":["role-1"]}`}, {method: http.MethodPost, path: "/api/admin/users/alice/roles", body: `{"roleIds":["role-1"]}`}, {method: http.MethodGet, path: "/api/admin/users/alice/permissions", body: ""}, + {method: http.MethodGet, path: "/api/admin/reports/catalog", body: ""}, {method: http.MethodGet, path: "/api/admin/reports/generate", body: ""}, {method: http.MethodPost, path: "/api/admin/reports/generate-multi", body: `{}`}, {method: http.MethodGet, path: "/api/admin/webhooks/audit", body: ""}, diff --git a/pkg/extensions/reporting_admin.go b/pkg/extensions/reporting_admin.go index 629031791..a9feee1af 100644 --- a/pkg/extensions/reporting_admin.go +++ b/pkg/extensions/reporting_admin.go @@ -10,6 +10,7 @@ import ( // ReportingAdminEndpoints defines the enterprise reporting admin endpoint surface. type ReportingAdminEndpoints interface { + HandleGetReportingCatalog(http.ResponseWriter, *http.Request) HandleGenerateReport(http.ResponseWriter, *http.Request) HandleGenerateMultiReport(http.ResponseWriter, *http.Request) HandleGetVMInventoryDefinition(http.ResponseWriter, *http.Request) diff --git a/pkg/reporting/catalog.go b/pkg/reporting/catalog.go new file mode 100644 index 000000000..cd192fa4c --- /dev/null +++ b/pkg/reporting/catalog.go @@ -0,0 +1,86 @@ +package reporting + +// ReportingFormatDefinition describes one supported output format for an +// operator-facing reporting surface. +type ReportingFormatDefinition struct { + Value ReportFormat `json:"value"` + Label string `json:"label"` +} + +// ReportingRangeDefinition describes one supported time window for historical +// performance reporting. +type ReportingRangeDefinition struct { + Key string `json:"key"` + Label string `json:"label"` + Description string `json:"description"` + WindowHours int `json:"windowHours"` +} + +// PerformanceReportDefinition describes the canonical performance reporting +// surface exposed to operators. +type PerformanceReportDefinition struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + SingleResourceEndpoint string `json:"singleResourceEndpoint"` + MultiResourceEndpoint string `json:"multiResourceEndpoint"` + SingleFilenamePrefix string `json:"singleFilenamePrefix"` + MultiFilenamePrefix string `json:"multiFilenamePrefix"` + Formats []ReportingFormatDefinition `json:"formats"` + DefaultFormat ReportFormat `json:"defaultFormat"` + Ranges []ReportingRangeDefinition `json:"ranges"` + DefaultRange string `json:"defaultRange"` + MultiResourceMax int `json:"multiResourceMax"` + SupportsMetricFilter bool `json:"supportsMetricFilter"` + SupportsCustomTitle bool `json:"supportsCustomTitle"` +} + +// ReportingCatalog describes the backend-owned admin reporting surface for the +// Pulse settings UI. +type ReportingCatalog struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + PerformanceReport PerformanceReportDefinition `json:"performanceReport"` + VMInventoryExport VMInventoryExportDefinition `json:"vmInventoryExport"` +} + +// DescribePerformanceReport returns the canonical definition for Pulse's +// historical performance reporting surface. +func DescribePerformanceReport() PerformanceReportDefinition { + return PerformanceReportDefinition{ + ID: "performance_reports", + Title: "Performance Reports", + Description: "Generate PDF summaries or CSV metric exports from historical monitoring data for one or more selected resources.", + SingleResourceEndpoint: "/api/admin/reports/generate", + MultiResourceEndpoint: "/api/admin/reports/generate-multi", + SingleFilenamePrefix: "report", + MultiFilenamePrefix: "fleet-report", + Formats: []ReportingFormatDefinition{ + {Value: FormatPDF, Label: "PDF Report"}, + {Value: FormatCSV, Label: "CSV Data"}, + }, + DefaultFormat: FormatPDF, + Ranges: []ReportingRangeDefinition{ + {Key: "24h", Label: "Last 24 Hours", Description: "Current-day operational summary for short-term regressions.", WindowHours: 24}, + {Key: "7d", Label: "Last 7 Days", Description: "Weekly trend window for recent performance changes.", WindowHours: 168}, + {Key: "30d", Label: "Last 30 Days", Description: "Monthly review window for sustained capacity or reliability shifts.", WindowHours: 720}, + }, + DefaultRange: "24h", + MultiResourceMax: 50, + SupportsMetricFilter: true, + SupportsCustomTitle: true, + } +} + +// DescribeReportingCatalog returns the canonical backend-owned settings +// definition for the advanced reporting feature. +func DescribeReportingCatalog() ReportingCatalog { + return ReportingCatalog{ + ID: "advanced_reporting", + Title: "Detailed Reporting", + Description: "Generate performance reports and current-state exports across infrastructure and workloads.", + PerformanceReport: DescribePerformanceReport(), + VMInventoryExport: DescribeVMInventoryExport(), + } +} diff --git a/pkg/reporting/catalog_test.go b/pkg/reporting/catalog_test.go new file mode 100644 index 000000000..8289909ad --- /dev/null +++ b/pkg/reporting/catalog_test.go @@ -0,0 +1,23 @@ +package reporting + +import "testing" + +func TestDescribeReportingCatalog_DefinesCanonicalSurfaces(t *testing.T) { + catalog := DescribeReportingCatalog() + + if catalog.ID != "advanced_reporting" { + t.Fatalf("catalog ID = %q, want advanced_reporting", catalog.ID) + } + if catalog.PerformanceReport.ID != "performance_reports" { + t.Fatalf("performance report ID = %q, want performance_reports", catalog.PerformanceReport.ID) + } + if catalog.PerformanceReport.MultiResourceMax != 50 { + t.Fatalf("multi-resource max = %d, want 50", catalog.PerformanceReport.MultiResourceMax) + } + if got := len(catalog.PerformanceReport.Ranges); got != 3 { + t.Fatalf("range count = %d, want 3", got) + } + if catalog.VMInventoryExport.ExportEndpoint != "/api/admin/reports/inventory/vms/export" { + t.Fatalf("vm inventory export endpoint = %q", catalog.VMInventoryExport.ExportEndpoint) + } +} diff --git a/pkg/reporting/vm_inventory.go b/pkg/reporting/vm_inventory.go index c24fe152b..1f0f6f381 100644 --- a/pkg/reporting/vm_inventory.go +++ b/pkg/reporting/vm_inventory.go @@ -24,6 +24,7 @@ type VMInventoryExportDefinition struct { Title string `json:"title"` Description string `json:"description"` Format ReportFormat `json:"format"` + ExportEndpoint string `json:"exportEndpoint"` FilenamePrefix string `json:"filenamePrefix"` Columns []InventoryExportColumnDefinition `json:"columns"` } @@ -58,6 +59,7 @@ func DescribeVMInventoryExport() VMInventoryExportDefinition { Title: "VM Inventory Export", Description: "Export the current fleet-wide VM inventory as CSV using the canonical runtime model. Includes VM identity, placement, CPU, memory allocation, disk allocation, and disk usage columns.", Format: FormatCSV, + ExportEndpoint: "/api/admin/reports/inventory/vms/export", FilenamePrefix: "vm-inventory", Columns: []InventoryExportColumnDefinition{ {Key: "resource_id", Label: "Resource ID", Description: "Canonical Pulse resource ID for the VM."},