Incorporate batch-start published Audit Webhooks guidance

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-05 08:06:22 +01:00
7 changed files with 94 additions and 1 deletions
@@ -5979,6 +5979,11 @@ canonical `SettingsPanel` shell, while
license/paywall lifecycle, webhook fetch/save flow, validation, paywall
tracking, and hidden-upgrade copy posture. The shell must not re-accumulate API
calls or paywall tracking inline.
Because it is the only webhook page in Settings, the shell opens with a
signpost, in both the gated and enabled states, saying audit webhooks carry
security events only and linking to the Alerts notifications tab for Discord,
Slack, ntfy or email delivery (issue #1344 tried to add a Discord webhook
here). The link is a plain anchor the app Router intercepts.
The same paid-runtime-required route applies to Audit Webhooks: missing
`audit_logging` caused by a community runtime must keep the panel reachable,
hide normal upgrade-plan prompts, and present the private Pulse Pro runtime
@@ -1329,6 +1329,10 @@ failure. If `runtime-capabilities` blocks `audit_logging` with
active Pro license needs the private Pulse Pro runtime, but they must not
expose license keys, billing identity, or plan-upgrade copy as part of that
security/privacy feature gate.
The Audit Webhooks panel also states its scope before any gate or endpoint
list: these webhooks carry security events only, and alert delivery lives in
alert notifications (`getAuditWebhookNotificationsSignpost`), so an operator
looking for Discord or Slack alerts is not left configuring an audit sink.
Audit-log storage availability is also a security/privacy trust boundary.
The `pkg/audit/` runtime package owns persistent audit-store classification:
transient SQLite busy/locked conditions must be retried and surfaced as
@@ -0,0 +1,36 @@
{
"version": 1,
"base_sha": "a28b1056459b2ae0fe01bbbe7aebd4c6ca7d3738",
"verified_at": "2026-10-05T06:23:30Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
"frontend-modern/src/utils/auditWebhookPresentation.ts"
],
"content_sha256": {
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx": "416aa81cc2a011b937f9ad1d85174db72020c61e2d0f3a6bcc0d2eee99419457",
"frontend-modern/src/utils/auditWebhookPresentation.ts": "f94b530c79dddedec64ecf4a9f5c80fe017580ea73ebcc479418c2bafaed3fba"
},
"routes": [
"/settings/security-webhooks",
"/alerts/notifications"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 390,
"height": 844
}
],
"states": [
"Audit Webhooks at 1440 with audit delivery not enabled on this instance: the panel opens with These carry security events only. To send alerts to Discord, Slack, ntfy or email, set up alert notifications, above the existing gate card. Before, the page gave no hint that alert webhooks live elsewhere",
"Phone at 390: the signpost wraps over two lines above the gate card",
"The enabled state (endpoint list) is not reachable on this mock instance, so its placement is covered by the settings architecture test"
],
"interactions": [
"Clicked alert notifications and landed on /alerts/notifications without a full page reload"
]
}
@@ -19,13 +19,33 @@ import {
getAuditWebhookEmptyStateCopy,
getAuditWebhookFeatureGateCopy,
getAuditWebhookLoadingState,
getAuditWebhookNotificationsSignpost,
} from '@/utils/auditWebhookPresentation';
import { pathForTab } from '@/features/alerts/types';
import { useAuditWebhookPanelState } from '@/components/Settings/useAuditWebhookPanelState';
interface AuditWebhookPanelProps {
canManage?: boolean;
}
const AuditWebhookNotificationsSignpost: Component = () => {
// A plain anchor: the app Router intercepts same-origin links, and the
// panel also renders outside a Router in its read-only tests.
const copy = getAuditWebhookNotificationsSignpost();
return (
<p class="text-sm text-muted leading-relaxed" data-audit-webhook-notifications-signpost>
{copy.text}{' '}
<a
href={pathForTab('destinations')}
class="font-medium text-blue-600 hover:underline dark:text-blue-400"
>
{copy.linkLabel}
</a>
.
</p>
);
};
export const AuditWebhookPanel: Component<AuditWebhookPanelProps> = (props) => {
const [urlToRemove, setUrlToRemove] = createSignal<string | null>(null);
let newUrlInputRef: HTMLInputElement | undefined;
@@ -56,7 +76,8 @@ export const AuditWebhookPanel: Component<AuditWebhookPanelProps> = (props) => {
return (
<SettingsPanel title="Audit Webhooks">
<Show when={!loading()} fallback={<div class="text-sm text-muted">Loading...</div>}>
<Card tone="info" padding="md">
<AuditWebhookNotificationsSignpost />
<Card tone="info" padding="md" class="mt-3">
<FeatureGateSection
title={featureGateCopy().title}
body={featureGateCopy().body}
@@ -85,6 +106,7 @@ export const AuditWebhookPanel: Component<AuditWebhookPanelProps> = (props) => {
Pulse can send a signed event payload whenever security-relevant activity occurs
(logins, settings changes, RBAC updates, and similar audit events).
</p>
<AuditWebhookNotificationsSignpost />
<Show
when={!loading()}
@@ -557,6 +557,13 @@ describe('settings architecture guardrails', () => {
expect(auditWebhookPanelSource).toContain('getAuditWebhookFeatureGateCopy({');
expect(auditWebhookPanelSource).toContain('paidRuntimeRequired: paidRuntimeRequired()');
expect(auditWebhookPanelSource).not.toContain('Audit Webhooks (Pro)');
// The only webhook page in Settings says it is not alert delivery and
// links to alert notifications, in both the gated and enabled states.
expect(auditWebhookPanelSource).toContain('getAuditWebhookNotificationsSignpost()');
expect(auditWebhookPanelSource).toContain("pathForTab('destinations')");
expect(auditWebhookPanelSource.match(/<AuditWebhookNotificationsSignpost \/>/g)).toHaveLength(
2,
);
expect(reportingPanelSource).toContain('if (!catalog || !state || showUpgradePrompts())');
expect(reportingPanelSource).toContain('title: `${state.title} unavailable`');
expect(reportingPanelSource).toContain(
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
getAuditWebhookNotificationsSignpost,
AUDIT_WEBHOOK_ENDPOINT_CARD_CLASS,
AUDIT_WEBHOOK_ENDPOINT_ICON_CLASS,
AUDIT_WEBHOOK_READONLY_NOTICE_CLASS,
@@ -42,4 +43,12 @@ describe('auditWebhookPresentation', () => {
expect(AUDIT_WEBHOOK_ENDPOINT_CARD_CLASS).toContain('bg-surface-alt');
expect(AUDIT_WEBHOOK_ENDPOINT_ICON_CLASS).toContain('bg-blue-100');
});
it('points people looking for alert delivery to alert notifications', () => {
const signpost = getAuditWebhookNotificationsSignpost();
expect(signpost.text).toContain('security events only');
expect(signpost.text).toContain('Discord');
expect(signpost.linkLabel).toBe('alert notifications');
expect(`${signpost.text} ${signpost.linkLabel}`).not.toContain(';');
});
});
@@ -51,6 +51,16 @@ export function getAuditWebhookFeatureGateCopy(
};
}
// People looking for alert delivery land here first because it is the only
// webhook page in Settings (issue #1344 tried to add a Discord webhook from
// it). The panel says what it is for and where alert notifications live.
export function getAuditWebhookNotificationsSignpost() {
return {
text: 'These carry security events only. To send alerts to Discord, Slack, ntfy or email, set up',
linkLabel: 'alert notifications',
} as const;
}
export function getAuditWebhookEmptyStateCopy(): AuditWebhookEmptyStateCopy {
return {
title: 'No audit webhooks configured yet.',