Align PMG collection guidance with saved-scope repair

Describe the integrated collector gates, explicit opt-out readback, legacy mail defaults and next-poll limits without treating dataset switches as a connection pause. Retain the verified v6.5.0 warning and incomplete-reading/alert safeguards in the existing guide and shipped mirror.

Contract-Neutral: Documentation and source-bound guidance tests only; no runtime, API, schema, permission or navigation change.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-08 07:53:14 +01:00
parent 003f78e726
commit c56cc6c838
3 changed files with 141 additions and 18 deletions
+33 -5
View File
@@ -37,14 +37,35 @@ by itself, and a missing candidate does not prove the gateway is unavailable.
Only scan networks you administer and intend to scan. Do not broaden a scan or
repeat discovery merely to diagnose missing statistics.
### Collection scope is not a no-request guarantee
### Collection scope and older releases
The connection form offers **Mail statistics & trends**, **Queue health
insights**, **Quarantine totals** and **Domain-level statistics**. Domain
statistics are off by default for a new connection. Do not use the individual
scope switches as a privacy or no-request boundary: mail, queue and quarantine
requests can still run with their options off. A saved checkbox or an absent
chart is not evidence that those requests stopped.
statistics are off by default for a new connection. In current development
source, the saved switches control these subsequent ordinary poll requests:
| Saved option | Reads it controls |
| --- | --- |
| Mail statistics & trends | Mail statistics, hourly mail counts and spam-score distribution. |
| Queue health insights | Each collected cluster node's queue status. |
| Quarantine totals | Both spam and virus quarantine counts. |
| Domain-level statistics | Relay-domain inventory and domain statistics. |
Saving all four options off preserves that choice when settings are reopened.
An older connection with no recorded scope keeps the legacy mail-statistics
default until its collection scope is explicitly saved; all-zero legacy settings
cannot distinguish an old opt-out from an unset default. Version, cluster and
configuration-backup reads are independent of these four switches: all four off
does not mean no PMG requests. Edits apply to the next poll, not requests already
in flight. Disabling collection removes coverage; it does not dismiss an
existing alert or establish recovery.
**Published v6.5.0 does not contain this scope correction.** In that version,
mail, queue and quarantine requests can still run with their options off;
domain statistics alone follow their switch. Do not rely on those three
switches as a privacy boundary in v6.5.0, or assume development-source behaviour
is present in an installed release. An absent chart is not evidence that
requests stopped.
If PMG collection must stop, pause the **PMG connection** in
**Settings → Infrastructure**, rather than deleting it or only disabling
@@ -77,6 +98,13 @@ badge or one populated panel does not prove that every dataset was read. Compare
the affected value and observation window with PMG's own existing view before
acting on an apparent empty queue or quarantine.
Current development source records downstream read failures as incomplete
collection while retaining the successful version connection. Missing readings
do not become observed zero totals or resolve their metric alerts. Partial
queue data can still raise the highest configured severity, but cannot clear
or downgrade an existing alert; quarantine recovery requires both categories.
These collector safeguards do not make every empty display a verified zero.
## Alerts
Open **Alerts → Thresholds → Proxmox** and find **Mail Gateway Thresholds**.
+33 -5
View File
@@ -37,14 +37,35 @@ by itself, and a missing candidate does not prove the gateway is unavailable.
Only scan networks you administer and intend to scan. Do not broaden a scan or
repeat discovery merely to diagnose missing statistics.
### Collection scope is not a no-request guarantee
### Collection scope and older releases
The connection form offers **Mail statistics & trends**, **Queue health
insights**, **Quarantine totals** and **Domain-level statistics**. Domain
statistics are off by default for a new connection. Do not use the individual
scope switches as a privacy or no-request boundary: mail, queue and quarantine
requests can still run with their options off. A saved checkbox or an absent
chart is not evidence that those requests stopped.
statistics are off by default for a new connection. In current development
source, the saved switches control these subsequent ordinary poll requests:
| Saved option | Reads it controls |
| --- | --- |
| Mail statistics & trends | Mail statistics, hourly mail counts and spam-score distribution. |
| Queue health insights | Each collected cluster node's queue status. |
| Quarantine totals | Both spam and virus quarantine counts. |
| Domain-level statistics | Relay-domain inventory and domain statistics. |
Saving all four options off preserves that choice when settings are reopened.
An older connection with no recorded scope keeps the legacy mail-statistics
default until its collection scope is explicitly saved; all-zero legacy settings
cannot distinguish an old opt-out from an unset default. Version, cluster and
configuration-backup reads are independent of these four switches: all four off
does not mean no PMG requests. Edits apply to the next poll, not requests already
in flight. Disabling collection removes coverage; it does not dismiss an
existing alert or establish recovery.
**Published v6.5.0 does not contain this scope correction.** In that version,
mail, queue and quarantine requests can still run with their options off;
domain statistics alone follow their switch. Do not rely on those three
switches as a privacy boundary in v6.5.0, or assume development-source behaviour
is present in an installed release. An absent chart is not evidence that
requests stopped.
If PMG collection must stop, pause the **PMG connection** in
**Settings → Infrastructure**, rather than deleting it or only disabling
@@ -77,6 +98,13 @@ badge or one populated panel does not prove that every dataset was read. Compare
the affected value and observation window with PMG's own existing view before
acting on an apparent empty queue or quarantine.
Current development source records downstream read failures as incomplete
collection while retaining the successful version connection. Missing readings
do not become observed zero totals or resolve their metric alerts. Partial
queue data can still raise the highest configured severity, but cannot clear
or downgrade an existing alert; quarantine recovery requires both categories.
These collector safeguards do not make every empty display a verified zero.
## Alerts
Open **Alerts → Thresholds → Proxmox** and find **Mail Gateway Thresholds**.
+75 -8
View File
@@ -22,6 +22,17 @@ def plain(text):
return " ".join(text.replace("**", "").split())
def go_block(text, opening):
"""Read one known source block, including its nested bodies."""
start = text.index(opening) + len(opening)
depth = 1
for end in range(start, len(text)):
depth += (text[end] == "{") - (text[end] == "}")
if depth == 0:
return text[start:end]
raise AssertionError(f"Unclosed source block: {opening}")
class PMGDocsTest(unittest.TestCase):
def setUp(self):
self.text = DOC.read_text(encoding="utf-8")
@@ -95,22 +106,78 @@ class PMGDocsTest(unittest.TestCase):
self.assertIn("does not prove that every dataset was read", self.guide)
self.assertIn("Unavailable evidence is unknown, not zero", self.guide)
def test_opt_out_guidance_does_not_invent_scope_enforcement(self):
def test_saved_scope_guidance_matches_each_owning_gate(self):
poll = (ROOT / "internal/monitoring/monitor_pbs_pmg.go").read_text()
poll = poll.split("func (m *Monitor) pollPMGInstance(", 1)[1]
# Do not lock in the runtime defect. If the owner adds these gates,
# the caveat can be reviewed/removed with that containing correction.
if any(f"if instanceCfg.{flag}" not in poll for flag in (
"MonitorMailStats", "MonitorQueues", "MonitorQuarantine",
)):
self.assertIn("requests can still run with their options off", self.guide)
self.assertIn("not evidence that those requests stopped", self.guide)
for condition, methods, description in (
("MailStatsEnabled()", ("GetMailStatistics", "GetMailCount", "GetSpamScores"),
"Mail statistics, hourly mail counts and spam-score distribution"),
("MonitorQueues", ("GetQueueStatus",), "Each collected cluster node's queue status"),
("MonitorQuarantine", ("GetQuarantineStatus",), "Both spam and virus quarantine counts"),
("MonitorDomainStats", ("ListRelayDomains", "GetDomainStatistics"),
"Relay-domain inventory and domain statistics"),
):
body = go_block(poll, f"if instanceCfg.{condition} {{")
for method in methods:
self.assertIn(f"client.{method}(", body)
self.assertEqual(body.count(f"client.{method}("), poll.count(f"client.{method}("))
self.assertIn(description, self.guide)
for phrase in ("saved switches control these subsequent ordinary poll requests",
"Edits apply to the next poll", "not requests already in flight",
"does not dismiss an existing alert or establish recovery"):
self.assertIn(phrase, self.guide)
def test_legacy_scope_and_explicit_false_readback_are_not_overpromised(self):
cfg = (ROOT / "internal/config/config.go").read_text()
default = go_block(cfg, "func (i PMGInstance) MailStatsEnabled() bool {")
self.assertIn("!i.MonitoringConfigured", default)
for flag in ("MonitorQueues", "MonitorQuarantine", "MonitorDomainStats"):
self.assertIn(f"!i.{flag}", default)
wire = (ROOT / "internal/api/configapi/config_handlers.go").read_text()
response = go_block(wire, "type NodeResponse struct {")
for flag in ("monitorMailStats", "monitorQueues", "monitorQuarantine", "monitorDomainStats"):
self.assertIn(f'json:"{flag}"', response)
self.assertNotIn(f'json:"{flag},omitempty"', response)
for phrase in ("Saving all four options off preserves that choice when settings are reopened",
"keeps the legacy mail-statistics default",
"until its collection scope is explicitly saved",
"cannot distinguish an old opt-out from an unset default"):
self.assertIn(phrase, self.guide)
def test_dataset_opt_out_is_not_a_connection_pause(self):
poll = (ROOT / "internal/monitoring/monitor_pbs_pmg.go").read_text()
poll = poll.split("func (m *Monitor) pollPMGInstance(", 1)[1]
for condition in ("MailStatsEnabled()", "MonitorQueues", "MonitorQuarantine", "MonitorDomainStats"):
body = go_block(poll, f"if instanceCfg.{condition} {{")
for method in ("GetVersion", "GetClusterStatus", "ListBackups"):
self.assertIn(f"client.{method}(", poll)
self.assertNotIn(f"client.{method}(", body)
for phrase in ("Version, cluster and configuration-backup reads are independent",
"all four off does not mean no PMG requests"):
self.assertIn(phrase, self.guide)
self.assertIn("if instanceCfg.Disabled", poll)
self.assertLess(poll.index("if instanceCfg.Disabled"), poll.index("client.GetVersion"))
for phrase in ("pause the PMG connection", "does not cancel a request already in flight",
"arrange independent coverage", "only the connection you deliberately paused"):
self.assertIn(phrase, self.guide)
def test_published_warning_is_bounded_to_the_named_release(self):
for phrase in ("Published v6.5.0 does not contain this scope correction",
"In that version, mail, queue and quarantine requests can still run",
"domain statistics alone follow their switch",
"assume development-source behaviour is present in an installed release"):
self.assertIn(phrase, self.guide)
def test_incomplete_collection_does_not_promise_zero_or_recovery(self):
for phrase in ("records downstream read failures as incomplete collection",
"retaining the successful version connection",
"Missing readings do not become observed zero totals",
"Partial queue data can still raise the highest configured severity",
"cannot clear or downgrade an existing alert",
"quarantine recovery requires both categories",
"do not make every empty display a verified zero"):
self.assertIn(phrase, self.guide)
def test_diagnostics_preserve_mail_privacy_and_separate_failure_classes(self):
for phrase in ("refusal is not a password verdict", "Certificate validation error",
"401/403", "specific failed request", "configured polling interval",