diff --git a/docs/release-control/v6/internal/records/self-hosted-paid-services-opt-in-surface-2026-04-25.md b/docs/release-control/v6/internal/records/self-hosted-paid-services-opt-in-surface-2026-04-25.md new file mode 100644 index 000000000..ba1fe601d --- /dev/null +++ b/docs/release-control/v6/internal/records/self-hosted-paid-services-opt-in-surface-2026-04-25.md @@ -0,0 +1,50 @@ +# Self-Hosted Paid Services Opt-In Surface Record + +- Date: `2026-04-25` +- Assertion: `RA5` +- Lane: `L2` +- Result: `pass` + +## Decision + +Ordinary self-hosted Pulse v6 users must not be presented with paid-service +prompts, Pro trial CTAs, plan upsells, or paid-only navigation by default. +Commercial surfaces remain available only when the user deliberately reaches +for them through an explicit handoff/direct route, is running in hosted mode, or +already has paid entitlement/activation/recovery state. + +This supersedes the earlier trial-first self-hosted monetization posture for +the normal v6 GA self-hosted app. Trial and checkout plumbing may remain for +support-only or externally initiated flows, but it is not a default in-app user +journey. + +## Product Boundary + +- Core self-hosted monitoring stays free and uncapped. +- Default Community/self-hosted UI stays quiet about paid services. +- Paid-only feature navigation is hidden unless the instance already has the + corresponding entitlement or recovery context. +- The billing route remains reachable directly for activation, recovery, and + explicit commercial handoff. +- Hosted mode can still opt in to promotional/commercial prompts through the + presentation policy contract. + +## Proof + +- `npm --prefix frontend-modern test -- --run src/stores/__tests__/sessionPresentationPolicy.test.ts src/stores/__tests__/license.test.ts src/components/Settings/__tests__/ProLicensePanel.test.tsx src/components/Settings/__tests__/settingsNavigation.integration.test.tsx src/components/Settings/__tests__/RBACPaywallPanels.test.tsx src/components/Settings/__tests__/AISettings.test.tsx src/pages/__tests__/AIIntelligence.test.tsx src/components/shared/__tests__/HistoryChart.test.tsx src/components/shared/__tests__/TrialBanner.test.tsx src/components/Settings/__tests__/useReportingPanelState.test.ts src/components/Settings/__tests__/ReportingPanel.test.tsx src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx src/components/Settings/__tests__/AgentProfilesPanel.test.tsx src/features/patrol/__tests__/patrolCommercialBoundary.test.ts` + - Result: pass, 14 files and 195 tests. +- `npm --prefix frontend-modern run type-check` + - Result: pass. +- `GOTOOLCHAIN=go1.25.9+auto go test ./internal/api -run 'TestContract_SecurityStatusPresentationPolicyDefaultsHideUpgradeOutsideHosted|TestContract_SecurityStatusIncludesSessionCapabilitiesDemoMode|TestSecurityStatusIncludesDemoModeSessionCapabilities|Test.*Trial|Test.*HostedEntitlementRefresh|TestContract_SelfHostedCommunityEntitlementsJSONSnapshot|TestContract_SelfHostedCommunityRuntimeCapabilitiesJSONSnapshot' -count=1` + - Result: pass. +- `GOTOOLCHAIN=go1.25.9+auto go test ./internal/cloudcp ./internal/cloudcp/stripe -count=1` + - Result: pass. +- `PULSE_E2E_USE_LOCAL_BACKEND=1 npm --prefix tests/integration test -- tests/58-self-hosted-trial-rate-limit-ui.spec.ts --project=chromium` + - Result: pass in Chromium against a managed local backend. + +## Known Unrelated State + +- Repository-wide frontend format check still reports pre-existing warnings + outside this slice. +- `frontendResourceTypeBoundaries.test.ts` still has a pre-existing Recovery + boundary failure unrelated to the paid-services visibility policy. diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index 804ff2623..d2a4cece3 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -307,7 +307,7 @@ }, { "id": "RA5", - "summary": "Non-paid users do not see paid-only navigation or pages unless the surface is explicitly promotional.", + "summary": "Ordinary self-hosted v6 users do not see paid service prompts, trial CTAs, plan upsells, or paid-only navigation by default; commercial surfaces are reachable only through explicit handoff, direct route, hosted mode, or active entitlement/recovery state.", "kind": "invariant", "blocking_level": "rc-ready", "proof_type": "hybrid", @@ -334,13 +334,47 @@ "test", "--", "--run", + "src/stores/__tests__/sessionPresentationPolicy.test.ts", + "src/stores/__tests__/license.test.ts", + "src/components/Settings/__tests__/ProLicensePanel.test.tsx", "src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", "src/components/Settings/__tests__/settingsNavigation.integration.test.tsx", - "src/pages/__tests__/AIIntelligence.test.tsx" + "src/components/Settings/__tests__/AISettings.test.tsx", + "src/components/Settings/__tests__/ReportingPanel.test.tsx", + "src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx", + "src/components/Settings/__tests__/AgentProfilesPanel.test.tsx", + "src/components/shared/__tests__/HistoryChart.test.tsx", + "src/components/shared/__tests__/TrialBanner.test.tsx", + "src/pages/__tests__/AIIntelligence.test.tsx", + "src/features/patrol/__tests__/patrolCommercialBoundary.test.ts" + ] + }, + { + "id": "self-hosted-paid-prompt-browser-visibility", + "run": [ + "env", + "PULSE_E2E_USE_LOCAL_BACKEND=1", + "npm", + "--prefix", + "tests/integration", + "test", + "--", + "tests/58-self-hosted-trial-rate-limit-ui.spec.ts", + "--project=chromium" ] } ], "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/self-hosted-paid-services-opt-in-surface-2026-04-25.md", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/components/Settings/__tests__/RBACPaywallPanels.test.tsx", @@ -351,10 +385,35 @@ "path": "frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx", "kind": "file" }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/useProLicensePanelState.ts", + "kind": "file" + }, { "repo": "pulse", "path": "frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx", "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/stores/__tests__/sessionPresentationPolicy.test.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/stores/licenseCommercial.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/stores/sessionPresentationPolicy.ts", + "kind": "file" + }, + { + "repo": "pulse", + "path": "tests/integration/tests/58-self-hosted-trial-rate-limit-ui.spec.ts", + "kind": "file" } ] }, @@ -2540,7 +2599,7 @@ "status": "partial", "completion": { "state": "bounded-residual", - "summary": "Commercial conversion surfaces are now coherent through the preview and app-side rehearsal path, while the production public flip and release-day rollout execution remain a separate promotion-time follow-up.", + "summary": "Commercial readiness is coherent for explicit preview, checkout handoff, paid-user activation/recovery, and hosted-mode paths, while default self-hosted app surfaces stay free-first and non-promotional; the production public flip and release-day rollout execution remain a separate promotion-time follow-up.", "tracking": [ { "kind": "lane-followup", @@ -2566,6 +2625,11 @@ "path": "docs/release-control/v6/internal/records/self-hosted-commercial-ga-coherence-2026-04-20.md", "kind": "file" }, + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/records/self-hosted-paid-services-opt-in-surface-2026-04-25.md", + "kind": "file" + }, { "repo": "pulse", "path": "docs/release-control/v6/internal/subsystems/cloud-paid.md", @@ -4156,7 +4220,7 @@ }, { "id": "commercial-ga-promotion-package", - "summary": "Track the production public flip and release-day execution work for the self-hosted commercial package now that preview, in-app entry, checkout handoff, and Pulse Account coherence are already proved.", + "summary": "Track the production public flip and release-day execution work for the self-hosted commercial package now that preview, external checkout handoff, paid-user recovery paths, and Pulse Account coherence are proved; default self-hosted app surfaces must stay free-first and non-promotional.", "owner": "project-owner", "status": "planned", "recorded_at": "2026-04-20", diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 14248cad9..90c4d38eb 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -686,6 +686,13 @@ capacity posture from public-preview runtime capabilities. Lifecycle-adjacent install or reporting surfaces may still depend on demo-safe capability flags, but they must not expect `monitored_system_capacity`, admission-freeze copy, or observed plan overage posture to survive the public-demo boundary. +The same presentation-policy split now governs paid lifecycle extensions in +ordinary self-hosted v6 installs. Agent profile management may remain an +entitled lifecycle surface, but default Infrastructure navigation must not +advertise agent-profile upgrades, trial prompts, or paid helper links while +`presentationPolicy.hideUpgrade` is true; it should stay on the free source +manager unless an explicit entitlement or recovery context makes the paid +lifecycle surface relevant. That same demo-hidden API boundary also keeps runtime-admin operations out of public lifecycle flows: `/api/diagnostics`, `/api/diagnostics/docker/prepare-token`, and `/api/logs/*` must return `404` diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 7a58f0b77..36f211200 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -835,6 +835,11 @@ as the canonical runtime contract for `demoMode`, `readOnly`, must therefore defer their first read until that policy has resolved, so public demos fail closed without probing hidden commercial routes during bootstrap. +For ordinary self-hosted v6 installs, that same security-status contract owns +the free-first commercial posture: `hideUpgrade` defaults to true outside +hosted mode, and API consumers must treat it as a prompt-suppression contract +for upgrade links, trial CTAs, plan upsells, and paid-only navigation rather +than as a billing entitlement change. That same contract split also makes the licensing boundary explicit: `/api/license/runtime-capabilities` is the public runtime feature contract, `/api/license/commercial-posture` is the non-billing upgrade/trial posture diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 4f18d0cbc..87c6c25e2 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -461,6 +461,13 @@ Community limit enforcement. from the local conversion store instead of exporting those event rows to the commercial service or reconstructing them from hosted checkout telemetry. +14. Keep ordinary self-hosted v6 commercial prompts opt-in. Cloud-paid runtime + may keep checkout, activation, recovery, and support-only trial plumbing + available for explicit handoffs and entitled installs, but default + self-hosted browser surfaces must honor `presentationPolicy.hideUpgrade` + and suppress Relay/Pro plan comparison, Pro trial CTAs, paid-only settings + navigation, and feature upsells unless hosted mode, direct intent, + activation/recovery state, or active entitlement makes them relevant. ## Current State @@ -1997,6 +2004,14 @@ public demos and other browser-first login surfaces do not emit avoidable `401` traffic before a session exists. Once the local login flow has written its bootstrap hint or the operator is landing on a protected route, the canonical state probe still owns authenticated runtime detection. +That same presentation-policy contract now owns the default self-hosted v6 +commercial posture. Outside hosted mode, ordinary self-hosted installs must +fail closed on in-app upgrade prompts: Relay/Pro plan comparison, Pro trial +CTAs, paid-only settings navigation, and history/feature upsells may render +only for explicit handoff/direct routes, activation or recovery state, hosted +mode, or an already entitled install. Trial checkout plumbing may remain for +support-only or externally initiated flows, but it is not a normal GA +self-hosted app journey. That public-demo commercial boundary also owns monitored-system preview unavailability wording. Browser presentation may keep the unavailable reason nullable until the formatting edge, but it must normalize the message through diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 7ef1058cb..0d29563b9 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -748,6 +748,14 @@ work extends shared components instead of creating new local variants. present in the shared app shell, top-level tab switches must reuse that state boundary instead of flashing a transient infrastructure page takeover between tabs. +35. Keep self-hosted paid-service prompts opt-in at the shared shell layer. + `settingsNavCatalog.ts`, `settingsNavVisibility.ts`, shared upgrade link + primitives, trial banners, and history-lock overlays must honor + `presentationPolicy.hideUpgrade` by hiding paid prompts by default on + ordinary self-hosted installs. Direct activation/recovery routes may + render their owned content, but sidebar discovery, trial CTAs, plan upsells, + and feature upgrade links must require hosted mode, explicit handoff, or + active entitlement. ## Current State @@ -2527,6 +2535,14 @@ response-header inference, hostname heuristics, or per-banner demo branching; the runtime bootstrap, shared session-capability store, and shared banner hooks stay on one canonical owner so suppression stays coherent across customer-facing surfaces. +That same session-presentation boundary owns the non-promotional self-hosted +v6 app posture. Settings navigation, shared upgrade links, trial banners, +history-lock overlays, and paid-feature gate primitives must honor resolved +`presentationPolicy.hideUpgrade` by hiding prompts by default on ordinary +self-hosted installs. Direct activation/recovery routes may still render their +owned content, but sidebar discovery, trial CTAs, plan upsells, and feature +upgrade links must not appear unless an explicit handoff, hosted-mode policy, +or active entitlement says they should. That same shared app-shell boundary now also owns assistant bootstrap silence on non-AI routes. `frontend-modern/src/useAppRuntimeState.ts`, `frontend-modern/src/App.tsx`, diff --git a/docs/release-control/v6/internal/subsystems/organization-settings.md b/docs/release-control/v6/internal/subsystems/organization-settings.md index 35edd2329..d224cdd76 100644 --- a/docs/release-control/v6/internal/subsystems/organization-settings.md +++ b/docs/release-control/v6/internal/subsystems/organization-settings.md @@ -245,3 +245,8 @@ setup completion surface allowed to bootstrap posture outside that shell. RBAC paywall state should also consume intent-level selectors such as `canOfferCommercialTrial()` from `frontend-modern/src/stores/licenseCommercial.ts` instead of reading raw posture fields locally. +Under the free-first self-hosted v6 policy, those RBAC gates must also honor +`presentationPolicy.hideUpgrade`: ordinary self-hosted users should not see +RBAC trial CTAs, upgrade copy, or paid-only Roles/Users/Audit navigation by +default. Direct routes may recover to the canonical free settings surface, and +entitled or hosted-mode installs may still render the governed RBAC surfaces. diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index eafa5ba7e..b0c0e84ff 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -196,6 +196,12 @@ but they must not trigger their own mount-time `loadCommercialPosture()` reads. Authenticated Patrol shells inherit that bootstrap from `frontend-modern/src/useAppRuntimeState.ts`, so Patrol-specific hooks do not quietly retake ownership of commercial fetch timing. +Under ordinary self-hosted v6, Patrol commercial affordances must also honor +the shared `presentationPolicy.hideUpgrade` contract. A free self-hosted +install may show Patrol runtime availability and configuration gaps, but it +must not show Pro trial CTAs, upgrade links, or paid helper copy unless hosted +mode, an explicit commercial handoff, or an active entitlement makes those +actions relevant. That degraded empty-state copy must also interpret the finding state rather than simply replaying the primary assessment sentence verbatim: when coverage is incomplete, the findings panel should tell the operator that Patrol has not diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 676132957..60e5644bd 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -141,6 +141,12 @@ querying, and the operator-facing storage health presentation layer. labels as `/api/resources` so storage and recovery do not momentarily switch between legacy and canonical infrastructure identities within one session. That same adjacent API boundary now also owns SSO outbound discovery and metadata fetch trust: storage- and recovery-adjacent surfaces may share `internal/api/sso_outbound.go`, `internal/api/saml_service.go`, and `internal/api/oidc_service.go`, but they must not fork separate metadata/discovery HTTP clients, redirect policies, or credential-file read rules when they depend on shared backend auth helpers. + That same adjacent security-status boundary now also owns paid prompt + suppression for storage/recovery-adjacent primitives. History and recovery + copy may describe unavailable local capability, but ordinary self-hosted v6 + installs must not reinterpret `presentationPolicy.hideUpgrade` as a license + upsell opportunity or surface paid history/recovery upgrade prompts by + default. 5. Route canonical storage/recovery resource selection through `frontend-modern/src/hooks/useUnifiedResources.ts` and the owning `unified-resources` contract That shared hook now also projects resource `clusterId` through the shared cluster-name helper, so storage and recovery links keep the same cluster-context label as other unified-resource consumers instead of rebuilding a local fallback chain. That shared hook plus the adjacent websocket/store adapter path must keep diff --git a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx index 9ab017175..4fb243995 100644 --- a/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx +++ b/frontend-modern/src/components/Settings/AIRuntimeControlsSection.tsx @@ -38,7 +38,12 @@ export const AIRuntimeControlsSection: Component onClick={() => state.setShowDiscoverySettings(!state.showDiscoverySettings())} >
- + stroke="currentColor" viewBox="0 0 24 24" > - + @@ -74,10 +84,7 @@ export const AIRuntimeControlsSection: Component
- +

@@ -240,7 +272,13 @@ export const AIRuntimeControlsSection: Component .

- +

{ setFormName, setShowModal, setShowSuggestModal, + showUpgradePrompts, showModal, showSuggestModal, startingTrial, @@ -83,43 +84,45 @@ export const AgentProfilesPanel: Component = () => { -

-
- + + +
+
+ +
+
+

Agent Profiles

+

Pro feature

+
-
-

Agent Profiles

-

Pro feature

-
-
-

- Create reusable configuration profiles for your agents. Manage settings like Docker - monitoring, logging levels, and reporting intervals from a central location. -

-
- - trackUpgradeClicked('settings_agent_profiles_panel', 'agent_profiles') - } - > - - {UPGRADE_ACTION_LABEL} - - - - -
- + + {UPGRADE_ACTION_LABEL} + + + + +
+ +
} >
diff --git a/frontend-modern/src/components/Settings/AuditLogPanel.tsx b/frontend-modern/src/components/Settings/AuditLogPanel.tsx index fcc5c1fd9..33dade700 100644 --- a/frontend-modern/src/components/Settings/AuditLogPanel.tsx +++ b/frontend-modern/src/components/Settings/AuditLogPanel.tsx @@ -73,6 +73,7 @@ export default function AuditLogPanel() { setUserFilter, setVerificationFilter, showUpgradePaywall, + showUpgradePrompts, startingTrial, submitPageInput, successFilter, @@ -157,14 +158,15 @@ export default function AuditLogPanel() { } > -
-
-
-

Audit Logging

-

- Persistent, searchable audit logs with cryptographic signature verification. -

-
+
+
+
+

Audit Logging

+

+ Persistent, searchable audit logs with cryptographic signature verification. +

+
+
-
+
+
-
-
- - Filters: -
- - setUserFilter(e.currentTarget.value)} - class="w-full sm:w-auto min-h-10 sm:min-h-10 px-3 py-2.5 text-sm border border-border rounded-md bg-surface text-base-content placeholder-gray-400" - /> - - - - - +
+
+ + Filters:
- 0}> -
- - {(chip) => ( - - )} - -
-
- - - {/* Verification Preferences */} - -
- setAutoVerifyEnabled(e.currentTarget.checked)} - /> -
-
+ 0}> +
+ + {(chip) => ( + + )} + +
+
+ + + {/* Verification Preferences */} + +
+ setAutoVerifyEnabled(e.currentTarget.checked)} + /> +
+
-
-
- - -
- {error()} -
-
- - -
- - {getAuditLogLoadingState().text} -
-
- - 0}> -
- Total: {totalEvents()} - Signed: {verificationSummary().signed} - Verified: {verificationSummary().verified} - Failed: {verificationSummary().failed} - Errors: {verificationSummary().error} - Unavailable: {verificationSummary().unavailable} - Unchecked: {verificationSummary().unchecked} + class="min-h-10 sm:min-h-10 w-24 rounded-md border border-border bg-surface px-2.5 py-2 text-sm text-base-content" + /> + + 0 disables, max 500 { const rect = e.currentTarget.getBoundingClientRect(); - const content = [ - 'Unsigned: no signature stored for this event', - 'Unavailable: verification not supported by the logger', - 'Not checked: signature exists but has not been verified', - 'Failed: signature mismatch or tampering detected', - 'Error: verification request failed', - ].join('\n'); - showTooltip(content, rect.left + rect.width / 2, rect.top, { - align: 'center', - direction: 'up', - maxWidth: 320, - }); + showTooltip( + 'Large verification batches can add load. Use a smaller limit on busy systems.', + rect.left + rect.width / 2, + rect.top, + { align: 'center', direction: 'up', maxWidth: 260 }, + ); }} onMouseLeave={() => hideTooltip()} > - Legend - - - {pageRangeText()} • Page {pageNumber()} of {totalPages()} +
-
- { - const presentation = getAuditEventStatusPresentation(event.success); - const Icon = presentation.icon; - return ; - }, - }, - { - key: 'timestamp', - label: 'Timestamp', - render: (event) => ( - {formatTimestamp(event.timestamp)} - ), - }, - { - key: 'event', - label: 'Event', - render: (event) => ( - - {event.event} - - ), - }, - { - key: 'user', - label: 'User', - render: (event) => {event.user || '-'}, - }, - { - key: 'ip', - label: 'IP', - hiddenOnMobile: true, - render: (event) => ( - {event.ip || '-'} - ), - }, - { - key: 'details', - label: 'Details', - hiddenOnMobile: true, - render: (event) => ( - {event.details || '-'} - ), - }, - { - key: 'verification', - label: 'Verification', - render: (event) => { - if (!event.signature) { - return Unsigned; - } - const state = verification()[event.id]; - const isVerifying = verifying()[event.id]; - const badge = getAuditVerificationBadgePresentation(state); +
+
- return ( -
- - {badge.label} - - } - > - Verifying… - - -
- ); - }, + +
+ {error()} +
+
+ + +
+ + {getAuditLogLoadingState().text} +
+
+ + 0}> +
+ Total: {totalEvents()} + Signed: {verificationSummary().signed} + Verified: {verificationSummary().verified} + Failed: {verificationSummary().failed} + Errors: {verificationSummary().error} + Unavailable: {verificationSummary().unavailable} + Unchecked: {verificationSummary().unchecked} + { + const rect = e.currentTarget.getBoundingClientRect(); + const content = [ + 'Unsigned: no signature stored for this event', + 'Unavailable: verification not supported by the logger', + 'Not checked: signature exists but has not been verified', + 'Failed: signature mismatch or tampering detected', + 'Error: verification request failed', + ].join('\n'); + showTooltip(content, rect.left + rect.width / 2, rect.top, { + align: 'center', + direction: 'up', + maxWidth: 320, + }); + }} + onMouseLeave={() => hideTooltip()} + > + + Legend + + + {pageRangeText()} • Page {pageNumber()} of {totalPages()} + +
+
+ { + const presentation = getAuditEventStatusPresentation(event.success); + const Icon = presentation.icon; + return ; }, - ]} - keyExtractor={(event) => event.id} - desktopMinWidth="900px" - /> -
+ }, + { + key: 'timestamp', + label: 'Timestamp', + render: (event) => ( + {formatTimestamp(event.timestamp)} + ), + }, + { + key: 'event', + label: 'Event', + render: (event) => ( + + {event.event} + + ), + }, + { + key: 'user', + label: 'User', + render: (event) => {event.user || '-'}, + }, + { + key: 'ip', + label: 'IP', + hiddenOnMobile: true, + render: (event) => ( + {event.ip || '-'} + ), + }, + { + key: 'details', + label: 'Details', + hiddenOnMobile: true, + render: (event) => ( + {event.details || '-'} + ), + }, + { + key: 'verification', + label: 'Verification', + render: (event) => { + if (!event.signature) { + return Unsigned; + } + const state = verification()[event.id]; + const isVerifying = verifying()[event.id]; + const badge = getAuditVerificationBadgePresentation(state); + + return ( +
+ + {badge.label} + + } + > + Verifying… + + +
+ ); + }, + }, + ]} + keyExtractor={(event) => event.id} + desktopMinWidth="900px" + /> +
-
-
- 0} - fallback={} +
+
+ 0} + fallback={} + > + + +

+ {getAuditLogEmptyState(activeFilterCount()).title} +

+

+ {getAuditLogEmptyState(activeFilterCount()).description} +

+ 0}> + - -
+ Clear all filters + +
+
-
-
- - -
-
- - - - -
+
+
+ +
+
+ + + + +
+
); diff --git a/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx b/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx index 1fa426993..57073e1d6 100644 --- a/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx +++ b/frontend-modern/src/components/Settings/AuditWebhookPanel.tsx @@ -43,6 +43,7 @@ export const AuditWebhookPanel: Component = (props) => { newUrl, saving, setNewUrl, + showUpgradePrompts, startingTrial, upgradeDestination, webhookUrls, @@ -61,31 +62,31 @@ export const AuditWebhookPanel: Component = (props) => {

{featureGateCopy().title}

-

- {featureGateCopy().body} -

+

{featureGateCopy().body}

-
- - trackUpgradeClicked('settings_audit_webhook_panel', 'audit_logging') - } - > - {UPGRADE_ACTION_LABEL} - - - - -
+ {UPGRADE_ACTION_LABEL} + + + + +
+
diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index b4657054d..d6853985a 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -2,6 +2,7 @@ import { Component, Match, Show, Switch, createEffect, createMemo, createSignal import { useLocation, useNavigate } from '@solidjs/router'; import X from 'lucide-solid/icons/x'; import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy'; +import { hasFeature, runtimeCapabilitiesLoaded } from '@/stores/license'; import { copyToClipboard } from '@/utils/clipboard'; import { notificationStore } from '@/stores/notifications'; import { Dialog } from '@/components/shared/Dialog'; @@ -349,15 +350,17 @@ const InfrastructureWorkspaceContent: Component = const renderAgentAddSlot = () => (
-
- -
+ +
+ +
+
diff --git a/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx b/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx index 0d0ff7541..3015ab66f 100644 --- a/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx +++ b/frontend-modern/src/components/Settings/RBACFeatureGateSection.tsx @@ -15,6 +15,7 @@ interface RBACFeatureGateSectionProps { canStartTrial: boolean; copy: RBACFeatureGateCopy; paywallLocation: RBACFeatureGateLocation; + showUpgradePrompts: boolean; startingTrial: boolean; onStartTrial: () => void | Promise; } @@ -26,25 +27,27 @@ export const RBACFeatureGateSection: Component = (p

{props.copy.title}

{props.copy.body}

-
- trackUpgradeClicked(props.paywallLocation, 'rbac')} - > - {UPGRADE_ACTION_LABEL} - - - - -
+ {UPGRADE_ACTION_LABEL} + + + + +
+
); diff --git a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx index 30d888e0b..d9d00eeec 100644 --- a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx @@ -35,10 +35,7 @@ export const RelaySettingsPanel: Component = (props) => // Pro feature gate if (!state.relayEnabled()) { return ( - + Loading...
}>
@@ -49,25 +46,27 @@ export const RelaySettingsPanel: Component = (props) =>

{RELAY_LICENSE_REQUIRED_MESSAGE}

-
- trackUpgradeClicked('settings_relay_panel', 'relay')} - > - Upgrade - - - - -
+ Upgrade + + + + +
+
@@ -76,20 +75,13 @@ export const RelaySettingsPanel: Component = (props) => } return ( - + {getSettingsConfigurationLoadingState().text}
} > - + Remote access settings are read-only for this account. @@ -108,7 +100,9 @@ export const RelaySettingsPanel: Component = (props) => pulse={state.connectionPresentation().pulse} />
-

{state.connectionPresentation().label}

+

+ {state.connectionPresentation().label} +

Instance: {state.status()!.instance_id}

@@ -121,9 +115,7 @@ export const RelaySettingsPanel: Component = (props) =>
-
- {state.status()!.last_error} -
+
{state.status()!.last_error}
diff --git a/frontend-modern/src/components/Settings/ReportingPanel.tsx b/frontend-modern/src/components/Settings/ReportingPanel.tsx index 14688eef7..c2db7c5e2 100644 --- a/frontend-modern/src/components/Settings/ReportingPanel.tsx +++ b/frontend-modern/src/components/Settings/ReportingPanel.tsx @@ -64,6 +64,7 @@ export function ReportingPanel() { setRange, setSelectedResources, setTitle, + showUpgradePrompts, startingTrial, title, upgradeDestination, @@ -78,8 +79,7 @@ export function ReportingPanel() { const supportsCustomTitle = () => performanceReport()?.supportsCustomTitle ?? false; const selectedRange = (): ReportingRangeValue => range() ?? performanceReport()!.defaultRange; const selectedFormat = (): ReportingFormat => format() ?? performanceReport()!.defaultFormat; - const optionalFieldCount = () => - Number(supportsMetricFilter()) + Number(supportsCustomTitle()); + const optionalFieldCount = () => Number(supportsMetricFilter()) + Number(supportsCustomTitle()); const optionalFieldGridClass = () => optionalFieldCount() > 1 ? 'grid grid-cols-1 gap-6 md:grid-cols-2' : 'grid grid-cols-1 gap-6'; @@ -99,10 +99,7 @@ export function ReportingPanel() { return (
- +

Loading reporting surfaces...

@@ -146,25 +143,29 @@ export function ReportingPanel() { )}
-
- trackUpgradeClicked('settings_reporting_panel', reportingCatalog()!.id)} - > - {UPGRADE_ACTION_LABEL} - - - - -
+ {UPGRADE_ACTION_LABEL} + + + + + + diff --git a/frontend-modern/src/components/Settings/RolesPanel.tsx b/frontend-modern/src/components/Settings/RolesPanel.tsx index 281509fe5..0e73bd7fa 100644 --- a/frontend-modern/src/components/Settings/RolesPanel.tsx +++ b/frontend-modern/src/components/Settings/RolesPanel.tsx @@ -37,6 +37,7 @@ export const RolesPanel: Component = () => { canStartTrial={state.featureGate.canStartTrial()} copy={state.featureGate.featureGateCopy()} paywallLocation="settings_roles_panel" + showUpgradePrompts={state.featureGate.showUpgradePrompts()} startingTrial={state.featureGate.startingTrial()} onStartTrial={state.featureGate.handleStartTrial} /> diff --git a/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx b/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx index 7c17ea7e1..7bdce9de9 100644 --- a/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx +++ b/frontend-modern/src/components/Settings/SSOProvidersPanel.tsx @@ -82,12 +82,13 @@ export const SSOProvidersPanel: Component = (props) => { fetchMetadataPreview, getUpgradeActionDestination, runtimeCapabilitiesLoaded, + showUpgradePrompts, trackUpgradeClicked, } = useSSOProvidersState(props); return (
- + setShowSamlUpsell(false)} @@ -147,7 +148,11 @@ export const SSOProvidersPanel: Component = (props) => { {/* License banner */} - +
@@ -207,7 +212,7 @@ export const SSOProvidersPanel: Component = (props) => { {getSSOProviderAddButtonLabel('saml')} - +