From b27387d8fc489a683d7fba591afb4bc1fc7bb4a5 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 07:29:43 +0100 Subject: [PATCH] Match probe help to the current observation-location editor Fresh editor and API inspection shows that local and remote paths can coexist, including in published v6.4.5. Explain the actual location picker, per-path uncertainty and safe canonical-versus-legacy update fields, and validate the linked target-field instructions in the Docs renderer. Contract-Neutral: Documentation, shipped mirrors and guidance verification only; existing observation and API semantics are unchanged. Change-source: pulse-maintainer --- docs/API.md | 7 +++++- docs/CONFIGURATION.md | 17 ++++++++----- docs/UNIFIED_AGENT.md | 16 +++++++----- .../browser-tests/probe-guidance.cjs | 10 ++++++-- frontend-modern/public/docs/API.md | 7 +++++- frontend-modern/public/docs/CONFIGURATION.md | 17 ++++++++----- frontend-modern/public/docs/UNIFIED_AGENT.md | 16 +++++++----- .../src/utils/__tests__/docsLinks.test.ts | 25 +++++++++++++++++++ 8 files changed, 87 insertions(+), 28 deletions(-) diff --git a/docs/API.md b/docs/API.md index e57302d49a..8ecbce7731 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1663,7 +1663,12 @@ Target payload fields: - `linkedResourceId` - Optional resource id hint for attaching the probe facet to an existing resource. - `certificateMonitoringDisabled` - Explicit HTTPS-only opt-out; certificate validity monitoring is enabled by default. - `certificateExpiryWarningDays` - HTTPS certificate expiry warning window; defaults to 30 days. -- `probeAgentId` - Optional registered host-agent ID that runs the check instead of the Pulse server; requires the Pro `external_probe` entitlement. Send an explicit empty string on update to return the check to local execution. +- `observationLocationIds` - Observation locations for this one logical check: `pulse:local` for this Pulse server and `agent:` for a connected agent. Agent locations require the Pro `external_probe` entitlement. Include `pulse:local` only when a local observation is wanted; agent-only sets do not also run locally. Each location retains separate evidence rather than treating one path failure as a universal outage. +- `probeAgentId` - Compatibility field for single-location clients. A non-empty registered host-agent ID selects that agent; an explicit empty string selects local execution. When updating only this field, omit `observationLocationIds`. For multi-location updates, send the complete desired `observationLocationIds` set and clear `probeAgentId` to `""` so a legacy value cannot override a single-location edit. + +External agents collect observations, but alert evaluation and notification +delivery still depend on the Pulse server. Missing reports are not proof of a +target outage. See [probe coverage and outage limits](CONFIGURATION.md#external-probes-pro). An explicit `linkedResourceId` is authoritative and fails closed when it cannot resolve. Without it, Pulse correlates only on one exact normalized IP diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index ef90f2a3b3..20cf7967ef 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -579,20 +579,25 @@ alerts, and resource projections keep one canonical protocol value. External probes change **where a check runs**, not where alerts are sent. By default, checks run on the Pulse server. With the Pro `external_probe` -entitlement, assign a connected Pulse agent using **Run from** in the UI -(`probeAgentId` in the API). An agent on a cloud VM or at another site can -observe a service from outside its local network. +entitlement, select one or more connected agents under **Observation locations** +in the check editor. An agent on a cloud VM or at another site can observe a +service from outside its local network. Keep **This Pulse server** selected if +you also want a local observation; deselect it for agent-only checks. Pulse keeps +each location's evidence separate, so a failure on one path is not a universal +outage. The API uses `observationLocationIds`; see the +[target fields](API.md#availability-checks). The assigned agent receives signed configuration, runs the check and reports -results back to Pulse. Only the currently assigned agent's results are accepted; -an assigned check is not also run locally. **The agent does not send +results back to Pulse. Only agents assigned to that target may submit results; +the server runs a local check only when **This Pulse server** is selected. +**The agent does not send notifications directly.** The Pulse server must be running and able to reach the notification destination to evaluate results and send alerts. | Situation | What the check can tell you | | --- | --- | | Pulse is running and receives probe results | Target observations are evaluated through the normal alert and notification policies. | -| Pulse is running but probe results stop arriving | The check becomes indeterminate: "no recent report from probe agent". This is missing evidence, not proof the target is down. | +| Pulse is running but probe results stop arriving | The affected location becomes indeterminate: "no recent report from probe agent". This is missing evidence, not proof the target is down. Other locations retain their own evidence. | | Pulse is stopped or cannot reach its notification destination | An external agent cannot deliver Pulse alerts in its place. Use an independent external watchdog for this failure. | The missing-report window is the longer of **five minutes or three check diff --git a/docs/UNIFIED_AGENT.md b/docs/UNIFIED_AGENT.md index dc9b567fae..6b8b322f15 100644 --- a/docs/UNIFIED_AGENT.md +++ b/docs/UNIFIED_AGENT.md @@ -303,11 +303,15 @@ storage nodes to avoid duplicate or ambiguously correlated readings. ## External Probes (Pro) With the Pro `external_probe` entitlement, availability checks configured in -Pulse can be assigned to run from a specific agent instead of the Pulse -server (Settings -> Monitoring -> Availability checks -> "Run from"). This is -how you observe a service from outside its local network: deploy the agent on -a machine elsewhere — a cloud VM, a Docker host at another location — and -assign checks to it. **The agent does not send notifications directly.** The +Pulse can run from one or more connected agents. In **Settings → Monitoring → +Availability checks**, select the agents under **Observation locations** in the +check editor. Keep **This Pulse server** selected for a local observation too, +or deselect it for agent-only checks. Each location keeps its own evidence; a +failure on one path is not a universal outage. + +To observe a service from outside its local network, deploy the agent on a +machine elsewhere — a cloud VM, a Docker host at another location — and assign +checks to it. **The agent does not send notifications directly.** The Pulse server must be running and able to reach the notification destination; target failures are evaluated there through your normal alert routes. @@ -317,7 +321,7 @@ configured interval, and results are delivered with its regular reports. Results waiting for delivery use a bounded **in-memory** queue of up to 200 observations across the agent's checks. Oldest pending observations are dropped when it fills, and an agent restart loses the queue; it is not a complete outage -record. If reports stop arriving, the check becomes indeterminate ("no recent +record. If reports stop arriving, the affected location becomes indeterminate ("no recent report from probe agent"). This is missing evidence, not proof the target is down. The missing-report window is the longer of **five minutes or three check intervals**. During normal server diff --git a/frontend-modern/browser-tests/probe-guidance.cjs b/frontend-modern/browser-tests/probe-guidance.cjs index a13e496443..6ee29e6ec1 100644 --- a/frontend-modern/browser-tests/probe-guidance.cjs +++ b/frontend-modern/browser-tests/probe-guidance.cjs @@ -50,6 +50,7 @@ const { chromium, webkit } = require('playwright'); const body = (await page.locator('article').innerText()).replace(/\s+/g, ' '); for (const text of ['The agent does not send notifications directly.', 'Pulse server must be running and able to reach the notification destination', + 'Observation locations', 'This Pulse server', 'not a universal outage', 'not proof the target is down', 'five minutes or three check intervals', '31 March 2027', 'Relay is no longer sold', 'not a permanent substitute']) { assert.ok(body.includes(text), text); @@ -96,6 +97,11 @@ const { chromium, webkit } = require('playwright'); await page.getByRole('heading', { name: 'External Probes (Pro)', exact: true }).waitFor(); await follow('configuration guide', '/docs/CONFIGURATION#external-probes-pro', 'External probes (Pro)', 'external-probes-pro'); + await follow('target fields', '/docs/API#availability-checks', + 'Availability Checks', 'availability-checks'); + assert.ok((await page.locator('article').innerText()).includes('clear probeAgentId to ""')); + await page.goBack(); + await page.getByRole('heading', { name: 'External probes (Pro)', exact: true }).waitFor(); await follow('external watchdog', '/docs/TROUBLESHOOTING#no-alert-when-pulse-power-or-internet-goes-down', 'No alert when Pulse, power or internet goes down', 'no-alert-when-pulse-power-or-internet-goes-down'); assert.deepEqual(errors, []); @@ -104,11 +110,11 @@ const { chromium, webkit } = require('playwright'); serverDependencyVisible: true, semanticContainedCoverageTable: true, noDocumentOverflow: true, queueAndRetirementLimitsVisible: true, keyboardAgentGuide: true, keyboardConfigurationGuide: true, - keyboardWatchdogLinks: 2, keyboardRetirementLink: true, errors }); + keyboardWatchdogLinks: 2, keyboardRetirementLink: true, keyboardCanonicalApiFields: true, errors }); await browser.close(); browser = undefined; } - const files = ['public/docs/CONFIGURATION.md', 'public/docs/UNIFIED_AGENT.md', + const files = ['public/docs/CONFIGURATION.md', 'public/docs/UNIFIED_AGENT.md', 'public/docs/API.md', 'browser-tests/probe-guidance.cjs', 'browser-tests/docs-fragment-navigation.tsx', 'src/features/docs/docMarkdown.ts', 'src/pages/Docs.tsx'].map((file) => ({ file, sha256: crypto.createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex') })); diff --git a/frontend-modern/public/docs/API.md b/frontend-modern/public/docs/API.md index e57302d49a..8ecbce7731 100644 --- a/frontend-modern/public/docs/API.md +++ b/frontend-modern/public/docs/API.md @@ -1663,7 +1663,12 @@ Target payload fields: - `linkedResourceId` - Optional resource id hint for attaching the probe facet to an existing resource. - `certificateMonitoringDisabled` - Explicit HTTPS-only opt-out; certificate validity monitoring is enabled by default. - `certificateExpiryWarningDays` - HTTPS certificate expiry warning window; defaults to 30 days. -- `probeAgentId` - Optional registered host-agent ID that runs the check instead of the Pulse server; requires the Pro `external_probe` entitlement. Send an explicit empty string on update to return the check to local execution. +- `observationLocationIds` - Observation locations for this one logical check: `pulse:local` for this Pulse server and `agent:` for a connected agent. Agent locations require the Pro `external_probe` entitlement. Include `pulse:local` only when a local observation is wanted; agent-only sets do not also run locally. Each location retains separate evidence rather than treating one path failure as a universal outage. +- `probeAgentId` - Compatibility field for single-location clients. A non-empty registered host-agent ID selects that agent; an explicit empty string selects local execution. When updating only this field, omit `observationLocationIds`. For multi-location updates, send the complete desired `observationLocationIds` set and clear `probeAgentId` to `""` so a legacy value cannot override a single-location edit. + +External agents collect observations, but alert evaluation and notification +delivery still depend on the Pulse server. Missing reports are not proof of a +target outage. See [probe coverage and outage limits](CONFIGURATION.md#external-probes-pro). An explicit `linkedResourceId` is authoritative and fails closed when it cannot resolve. Without it, Pulse correlates only on one exact normalized IP diff --git a/frontend-modern/public/docs/CONFIGURATION.md b/frontend-modern/public/docs/CONFIGURATION.md index ef90f2a3b3..20cf7967ef 100644 --- a/frontend-modern/public/docs/CONFIGURATION.md +++ b/frontend-modern/public/docs/CONFIGURATION.md @@ -579,20 +579,25 @@ alerts, and resource projections keep one canonical protocol value. External probes change **where a check runs**, not where alerts are sent. By default, checks run on the Pulse server. With the Pro `external_probe` -entitlement, assign a connected Pulse agent using **Run from** in the UI -(`probeAgentId` in the API). An agent on a cloud VM or at another site can -observe a service from outside its local network. +entitlement, select one or more connected agents under **Observation locations** +in the check editor. An agent on a cloud VM or at another site can observe a +service from outside its local network. Keep **This Pulse server** selected if +you also want a local observation; deselect it for agent-only checks. Pulse keeps +each location's evidence separate, so a failure on one path is not a universal +outage. The API uses `observationLocationIds`; see the +[target fields](API.md#availability-checks). The assigned agent receives signed configuration, runs the check and reports -results back to Pulse. Only the currently assigned agent's results are accepted; -an assigned check is not also run locally. **The agent does not send +results back to Pulse. Only agents assigned to that target may submit results; +the server runs a local check only when **This Pulse server** is selected. +**The agent does not send notifications directly.** The Pulse server must be running and able to reach the notification destination to evaluate results and send alerts. | Situation | What the check can tell you | | --- | --- | | Pulse is running and receives probe results | Target observations are evaluated through the normal alert and notification policies. | -| Pulse is running but probe results stop arriving | The check becomes indeterminate: "no recent report from probe agent". This is missing evidence, not proof the target is down. | +| Pulse is running but probe results stop arriving | The affected location becomes indeterminate: "no recent report from probe agent". This is missing evidence, not proof the target is down. Other locations retain their own evidence. | | Pulse is stopped or cannot reach its notification destination | An external agent cannot deliver Pulse alerts in its place. Use an independent external watchdog for this failure. | The missing-report window is the longer of **five minutes or three check diff --git a/frontend-modern/public/docs/UNIFIED_AGENT.md b/frontend-modern/public/docs/UNIFIED_AGENT.md index dc9b567fae..6b8b322f15 100644 --- a/frontend-modern/public/docs/UNIFIED_AGENT.md +++ b/frontend-modern/public/docs/UNIFIED_AGENT.md @@ -303,11 +303,15 @@ storage nodes to avoid duplicate or ambiguously correlated readings. ## External Probes (Pro) With the Pro `external_probe` entitlement, availability checks configured in -Pulse can be assigned to run from a specific agent instead of the Pulse -server (Settings -> Monitoring -> Availability checks -> "Run from"). This is -how you observe a service from outside its local network: deploy the agent on -a machine elsewhere — a cloud VM, a Docker host at another location — and -assign checks to it. **The agent does not send notifications directly.** The +Pulse can run from one or more connected agents. In **Settings → Monitoring → +Availability checks**, select the agents under **Observation locations** in the +check editor. Keep **This Pulse server** selected for a local observation too, +or deselect it for agent-only checks. Each location keeps its own evidence; a +failure on one path is not a universal outage. + +To observe a service from outside its local network, deploy the agent on a +machine elsewhere — a cloud VM, a Docker host at another location — and assign +checks to it. **The agent does not send notifications directly.** The Pulse server must be running and able to reach the notification destination; target failures are evaluated there through your normal alert routes. @@ -317,7 +321,7 @@ configured interval, and results are delivered with its regular reports. Results waiting for delivery use a bounded **in-memory** queue of up to 200 observations across the agent's checks. Oldest pending observations are dropped when it fills, and an agent restart loses the queue; it is not a complete outage -record. If reports stop arriving, the check becomes indeterminate ("no recent +record. If reports stop arriving, the affected location becomes indeterminate ("no recent report from probe agent"). This is missing evidence, not proof the target is down. The missing-report window is the longer of **five minutes or three check intervals**. During normal server diff --git a/frontend-modern/src/utils/__tests__/docsLinks.test.ts b/frontend-modern/src/utils/__tests__/docsLinks.test.ts index a2a9f683e3..9d063ad55f 100644 --- a/frontend-modern/src/utils/__tests__/docsLinks.test.ts +++ b/frontend-modern/src/utils/__tests__/docsLinks.test.ts @@ -117,6 +117,31 @@ describe('docsLinks', () => { } }); + it('matches the existing observation-location editor and compatibility fields', () => { + for (const guide of probeGuides()) { + expect(guide).toContain('Observation locations'); + expect(guide).toContain('This Pulse server'); + expect(guide).toContain('not a universal outage'); + expect(guide).not.toContain('Run from'); + expect(guide).not.toContain('an assigned check is not also run locally'); + } + const api = readFileSync(path.join(repoRoot, 'docs', 'API.md'), 'utf8'); + expect(api).toContain('`observationLocationIds` - Observation locations'); + expect(api).toContain('`pulse:local` for this Pulse server'); + expect(api).toContain('`agent:` for a connected agent'); + expect(api).toContain('omit `observationLocationIds`'); + expect(api).toContain('clear `probeAgentId` to `""`'); + const editor = readFileSync(path.join(frontendRoot, 'src', 'components', 'Settings', + 'ConnectionEditor', 'CredentialSlots', 'AvailabilityTargetSlot.tsx'), 'utf8'); + expect(editor).toContain('Observation locations'); + expect(editor).toContain('observationLocationIds: [...form.observationLocationIds]'); + const handler = readFileSync( + path.join(repoRoot, 'internal', 'api', 'availability_handlers.go'), 'utf8', + ); + expect(handler).toContain('compatibility.ObservationLocationIDs == nil && compatibility.ProbeAgentID != nil'); + expect(handler).toContain('agentID != "" && len(*compatibility.ObservationLocationIDs) == 1'); + }); + it('separates server removal from persistent-data erasure', () => { const installation = readFileSync(path.join(repoRoot, 'docs', 'INSTALL.md'), 'utf8'); const removal = installation.split('## 🗑️ Uninstall')[1];