diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md index f1df8dcbd..f1fbc51b3 100644 --- a/docs/PRIVACY.md +++ b/docs/PRIVACY.md @@ -22,9 +22,12 @@ While mock/demo fixture mode is enabled, Pulse suppresses outbound telemetry ent #### How to disable +- During first-run setup, switch off **Usage statistics** on the admin-account step, or - **Settings → System → General → Outbound usage telemetry** (toggle off), or - Set the environment variable `PULSE_TELEMETRY=false` +The first startup ping is sent about two minutes after Pulse starts. The setup and Settings switches stop every later ping; setting `PULSE_TELEMETRY=false` before the first start prevents the first one as well. + #### How to inspect or rotate it - **Settings → System → General → Preview payload** shows the exact heartbeat JSON Pulse would send with the current runtime state. @@ -346,6 +349,16 @@ added. - No alert content, AI prompts, chat messages, tool names, tool inputs, tool outputs, command text, action output, or token values - No names, email addresses, account identifiers, or other intentionally identifying personal content +#### What it is not used for + +- It is not sold, licensed, or shared with anyone else. Pulse's maintainer is the only reader, and the only destination is Pulse's own license server. +- It is not used for advertising, marketing, or outreach of any kind. Nothing in it can address you. +- It is not linked to a Pulse account, license key, purchase, or email address. The license server never joins telemetry rows to those records. +- It is not used to single out an install. Reads are aggregate, and the install ID rotates every 30 days. +- It is not kept: rows are deleted after 90 days. + +If any of this ever changes, that is a change in kind under **Payload changes** above and comes with an in-app notice before it takes effect. + #### Install ID rotation The telemetry install ID is pseudonymous, is not tied to a Pulse account, and rotates automatically every 30 days. @@ -359,6 +372,29 @@ highest activation stage reached. This local state contains no user, account, resource, URL, or content identifiers. It exists so daily pings can report buckets instead of exporting a sequence of setup events. +#### Payload changes + +Every change to the payload bumps the schema version, is listed here with its date, and appears in the release notes of the first release that carries it. Pulse does not interrupt existing installations with an in-app notice for a new counter inside an already-disclosed category; **Preview payload** in Settings always shows the exact current contract. An in-app notice is reserved for a change in kind: a new identifier, a new class of data, or a change to retention or handling. + +| Schema | Date | Change | +|--------|------|--------| +| 17 | 2026-09-02 | Closed Patrol provider class, effective Patrol autonomy level, coarse 30-day Patrol token buckets, and per-outcome investigation counts | +| 16 | 2026-08-30 | Four content-free workload-history adoption counters, each counted at most once per browser session | +| 15 | 2026-08-29 | Notification destination HTTP 5xx failures separated from rejected HTTP 4xx responses | +| 14 | 2026-08-29 | Identity-free alert quality outcomes in closed severity, age, and resolution-time buckets, with tenant denominators | +| 13 | 2026-08-29 | Local UI/API service observation plus the immediately previous release observation | +| 12 | 2026-08-27 | Patrol-origin action funnel counters | +| 11 | 2026-08-24 | Node connection test attempt and failure counts | +| 10 | 2026-08-21 | Patrol runtime blocked cause, from a fixed category list | +| 9 | 2026-08-19 | Refusals with no machine reason code separated from refusals with an unrecognised code | +| 8 | 2026-08-13 | Agent-side pre-mutation refusals split into target-change, prerequisite, and invalid-contract categories | +| 7 | 2026-08-05 | `audit_reads_30d` replaces `audit_logging_persistent` and `audit_events_30d` | +| 6 | 2026-08-05 | Licensed-feature adoption counts; the never-populated Patrol autofix counter removed | +| 5 | 2026-07-29 | Bounded, content-free notification failure classes | +| 4 | 2026-07-27 | Complete approved-action outcome accounting, fixed pre-dispatch refusal categories, and verified finding-resolution linkage | +| 3 | 2026-07-23 | `notification_failures_7d` becomes a terminal-delivery count | +| 2 | 2026-07-23 | Coarse deployment, lifecycle, and estate-size buckets plus aggregate alert and notification outcome signals | + #### Source code The telemetry implementation is in [`internal/telemetry/telemetry.go`](../internal/telemetry/telemetry.go). You can read the `Ping` struct to see every field that is transmitted. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 82c3bc554..2ad16bccf 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1445,12 +1445,10 @@ artifact-selection behaviour. later installed release. Automatic release communication is limited to a compact non-blocking update notice; the detailed changelog may open only after explicit operator action. Preparing that notice records the version - immediately so a reload cannot turn it into a recurring prompt. When the - one-time telemetry disclosure owns the same session, it suppresses the - lower-priority release notice instead of creating consecutive notices. + immediately so a reload cannot turn it into a recurring prompt. `frontend-modern/src/utils/localStorage.ts` owns that browser-session notice - reservation boundary so the release notice, telemetry disclosure, and - GitHub gratitude prompt cannot create a one-two sequence. The post-update + reservation boundary so the release notice and the GitHub gratitude prompt + cannot create a one-two sequence. The post-update surface must not reuse the Highlights summary as its content, and must stay silent for a first baseline, malformed or development versions, missing releases, and releases without categorized changes. @@ -1458,14 +1456,14 @@ artifact-selection behaviour. keeps it to at most three short plain-text bullets of no more than 140 characters each, with links, code, issue references, and nested structure reserved for the categorized or full release notes. - The same post-update communication boundary owns the one-time schema-v2 - telemetry payload notice. It must use a non-blocking shared notice banner, - appear only for existing installations on a published build, stay silent - for fresh installs and development/source builds, persist acknowledgement, - and provide direct payload-preview, disable, and privacy-disclosure actions. - The corresponding next-release disclosure must enumerate the added coarse - signal categories and exclusions without inventing a release version before - the packet is cut. + The post-update communication boundary does not announce telemetry payload + changes. Those are disclosed in the dated `Payload changes` section of + `docs/PRIVACY.md` and in the release notes of the first release that carries + them, so `frontend-modern/src/components/WhatsNewCard.tsx` renders only the + release notice and the notice reservation recognizes only the release notice + and the GitHub gratitude prompt as owners. A retired payload-update banner + paired a disclosure with a one-click disable action; `security-privacy` now + owns when an in-app telemetry notice is warranted and forbids that pairing. 5. Add or change local dev-runtime orchestration, managed ownership, browser-runtime proof wiring, frontend/backend coherence diagnostics, canonical developer entry wrappers, deterministic dev auth seeding, dependency manifest floors, frontend build chunking, or dev-runtime helper control surfaces through `scripts/hot-dev.sh`, `scripts/hot-dev-bg.sh`, `scripts/lib/hot-dev-runtime.sh`, `scripts/lib/hot-dev-auth.sh`, `scripts/dev-deploy-agent.sh`, `Makefile`, `package.json`, `package-lock.json`, `frontend-modern/package.json`, `frontend-modern/package-lock.json`, `frontend-modern/vite.config.ts`, `go.mod`, `go.sum`, `scripts/dev-check.sh`, `scripts/toggle-mock.sh`, `scripts/clean-mock-alerts.sh`, `scripts/dev-launchd-setup.sh`, `scripts/dev-launchd-wrapper.sh`, `scripts/run_demo_public_browser_smoke.sh`, `scripts/demo_public_browser_smoke.cjs`, `scripts/com.pulse.hot-dev.plist.template`, `tests/integration/scripts/managed-dev-runtime.mjs`, `tests/integration/playwright.config.ts`, `tests/integration/tests/helpers.ts`, `tests/integration/tests/runtime-defaults.ts`, `tests/integration/README.md`, and `tests/integration/QUICK_START.md` First-run browser helpers are part of that dev-runtime proof boundary. They must preserve the setup-created API token in the shared runtime state, prefer diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index e7894ab66..78da33370 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -927,7 +927,15 @@ AGENT_SURFACE_ID_PULSE_MCP)` and `getAgentSurfaceToolPosturePresentation`, may be named, while hostnames, credentials, infrastructure identifiers, URLs, paths, locale, browser events, prompts, chat messages, command text, action output, token values, and personal information must stay explicitly - excluded. + excluded. `Preview payload` is the panel's primary action, because the exact + runtime payload is the disclosure an operator can verify; the enable toggle + and `Reset ID` stay secondary controls, and the summary copy opens with what + the data is for before enumerating categories and exclusions. The shared + settings shell no longer accepts a `telemetryAction` deep link that changes + the preference on arrival; the preference changes only from the panel. + The summary copy also states what the data is never used for (sold, + shared, advertising, account or license linkage) in every locale, with the + `security-privacy` disclosure as the source of those statements. 8. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` shared with `security-privacy`: the authentication settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. 9. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` shared with `security-privacy`: the security overview settings surface is both a security/privacy control surface and a canonical settings-shell presentation boundary. These settings panels consume the privileged security-status projection, diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 5f7f73a9c..b94254816 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -1612,12 +1612,31 @@ That same telemetry trust boundary must remain operator-inspectable in-product: the shared system settings surface may preview only the exact runtime payload Pulse would send, and it must allow an operator to rotate the local telemetry install ID immediately without waiting for the scheduled 30-day window. -An existing installation's first published schema-v2 upgrade must also receive -a one-time, non-blocking notice that names the coarse payload expansion and -links directly to the exact preview, the disable action, and the governed -privacy disclosure. Fresh installs stay silent because setup already presents -the current disclosure. Acknowledging the notice may persist locally, but it -must not change the operator's telemetry preference by itself. +Payload changes are disclosed through the dated `Payload changes` section of +that same governed privacy disclosure and through the release notes of the +first release that carries them, and every change must bump the schema +version. Existing installations are not interrupted with an in-app notice for +a new counter inside an already-disclosed category; the Settings payload +preview is the live disclosure. An in-app notice is reserved for a change in +kind (a new identifier, a new class of data, or a change to retention or +handling), and such a notice must not carry a one-click disable action: a +disable control attached to a disclosure reads as a prompt to opt out rather +than as information. First-run setup must present the telemetry choice as a +real control on the first authenticated step, defaulting to enabled and +applied through the canonical system-settings path once the admin token +exists; it must not instruct the operator to set an environment variable +before a process that has already started. That instruction belongs in the +privacy disclosure and install docs, where the reader can still act on it. +Setup, disclosure, and Settings copy must say what the data is for before +saying how to turn it off, and must name concrete exclusions (hostnames, +credentials, IP addresses) rather than only abstract categories. +The same copy, and the governed privacy disclosure's `What it is not used +for` section, must also state the negative uses plainly: not sold or shared, +not used for advertising or outreach, not linked to a Pulse account, license, +purchase, or email address, and not used to single out an install. Those are +statements of fact about the license-server path (which never joins telemetry +rows to license or customer records), so any change to that path is a change +in kind that requires the in-app notice above before it takes effect. That same governed privacy disclosure must also state the current server-side telemetry retention and handling rules plainly. If the license-server path retains telemetry rows for a fixed window or uses client IPs transiently for diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 45a98cb0f..4408a7f5c 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,115 +1,37 @@ { "version": 1, - "base_sha": "3016bc72a0e64c43a1c4e49d805b59874e875643", - "verified_at": "2026-09-02T12:15:21Z", + "base_sha": "ef460aa6542a41728e2fe3e4a8266488c919b0be", + "verified_at": "2026-09-02T18:04:36Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/shared/SearchInput.tsx", - "frontend-modern/src/features/docker/DockerAlertsTable.tsx", - "frontend-modern/src/features/docker/DockerConfigsTable.tsx", - "frontend-modern/src/features/docker/DockerContainersTable.tsx", - "frontend-modern/src/features/docker/DockerHostsTable.tsx", - "frontend-modern/src/features/docker/DockerImagesTable.tsx", - "frontend-modern/src/features/docker/DockerNetworksTable.tsx", - "frontend-modern/src/features/docker/DockerSecretsTable.tsx", - "frontend-modern/src/features/docker/DockerServicesTable.tsx", - "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx", - "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx", - "frontend-modern/src/features/docker/DockerTasksTable.tsx", - "frontend-modern/src/features/docker/DockerVolumesTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx", - "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx", - "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", - "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx", - "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx", - "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx", - "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx", - "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx", - "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx", - "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx", - "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx", - "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx", - "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx", - "frontend-modern/src/features/vmware/VsphereActivityTable.tsx", - "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx", - "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx", - "frontend-modern/src/features/vmware/VsphereHostsTable.tsx", - "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx" + "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx", + "frontend-modern/src/components/Settings/Settings.tsx", + "frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx", + "frontend-modern/src/components/WhatsNewCard.tsx", + "frontend-modern/src/i18n/messages.de.ts", + "frontend-modern/src/i18n/messages.es.ts", + "frontend-modern/src/i18n/messages.ts", + "frontend-modern/src/utils/localStorage.ts" ], "content_sha256": { - "frontend-modern/src/components/shared/SearchInput.tsx": "ba88362103f5479034a79a9fb888e87187d39876209e624421088bfb1bced26a", - "frontend-modern/src/features/docker/DockerAlertsTable.tsx": "5ea264b2f1bcdd5d2561646e9e19d704fcb948fbcebd34f7ec7a2c91b564c626", - "frontend-modern/src/features/docker/DockerConfigsTable.tsx": "4abd9a32b9540dbf049e0a7a9ebc914c33519525ee5ddf43cf50c4713767a7b1", - "frontend-modern/src/features/docker/DockerContainersTable.tsx": "64d9092d9b0537a42352b21ce038865251a80d1a23ee90d4defced0f50e4dbd3", - "frontend-modern/src/features/docker/DockerHostsTable.tsx": "addda2de56331ada4002c81a73e2005a9b5cb336d7575a912216bdf794a2667c", - "frontend-modern/src/features/docker/DockerImagesTable.tsx": "fb8a45ef41eaa926b18a1a2d0f5e5586730431cd9b2ab7db79dbf57f343e3a6f", - "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "e824f88e2b09c3afe843d7ddc236c26ee1218a5bf1506a8aad256d2550e6cd97", - "frontend-modern/src/features/docker/DockerSecretsTable.tsx": "c3fdbc2976a84967d0682e078e5f5f8943f5658b194e6b4a30b9c7208cf56f8f", - "frontend-modern/src/features/docker/DockerServicesTable.tsx": "b936d55463a05b50ca60e6dccc2471dc142f02863fe077833a299aabfff308b3", - "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx": "25c821633dd0d82223968a0d29a196e5afc7da8105f35eb897488a40bfba965d", - "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx": "9b702a777e0572b6014b32fe64773bac469c84140f925eecdc86911495da0222", - "frontend-modern/src/features/docker/DockerTasksTable.tsx": "d8b52db2416a11ac76ede1407c3ab7521f7633a160052049a6033bd253890608", - "frontend-modern/src/features/docker/DockerVolumesTable.tsx": "41793497dcef26c328de418807bdab3e42b583631452ed164edc89a5dbd41add", - "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx": "59339fbd307b7e77386df84db34fe59cbb47bd8f8e9ea478afeab90f8ae9e094", - "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "5f521812a318b54e5d3db1ed81b3e778c59cedd93e81ed535c9d3e5f4c9df1a7", - "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx": "1f50a7ff4ddc2acbb4162ccecddc08bfbd6b39d7d8f32bf6bf68a63b5864a20b", - "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "61a8629ec241659a178760b6e97f4a137204ddbc9f52f4e4baecb6b719df527d", - "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "171a22ff20c69edd59be65bf4dcf2af1e2765fa4156e511e1689b0c9eab73db5", - "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "e98b2480e091269dfa342d7bbef6976663f626e6bd4335d38a85a40edb3feb26", - "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "1fa633f5dfb149d9a8f08182b08632fa6fdcc38ba91cd20c6ab5c17cd9244e36", - "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "318d20f8b896ae94f9c34ce521b9f105bfeebc8c2b1d06489930e055f4986710", - "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "cbbee7fcad2e6c17c405b70c69c8dd476e36a6dcdaa937a9372446336bcee277", - "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "a94c17b87e120be4051e70c20e7a63716309e94adc6c7b48550ab8857be169cf", - "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "eaefd6a7c9ef5e4e43c29afc9c7de1a1fec513a02bf17f110f1c3d31962054b5", - "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "1cd2c8fd0e5edc4db005a8d3f9fa0a49732b2aac3d829288e1e6c2ec5b8c9bdd", - "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "ff8de65244d1f36ccb9e628be2c51404c6c3aea3a5f57905a98cd218754fcb76", - "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx": "fe9669116eb81930f93b6a978ecfd7b9a9fa75d96d02843f3f2fe02256bf2902", - "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "97db101cb72d4813e75442da69398397a8b2d36501bfe13b7826820499aaf540", - "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx": "f52833e1b5fb978b560925eaaa2a70583e0e43e400c3de58962b6c423aed143b", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "a66016ab4fac48e7f1d5fa01c320cfbcb5ee062477d8cc93e22e42277a023fc3", - "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "0ee88b539ef6fbd73844c09be2a23e6eb032b6098f557c7d50c7d052148c0577", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "fe75fa3c4764b9d878a53594919e2dc352d2761a46d0c291205c48ebedf8877e", - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "e68c504226a0f63b07d84c2200f18770a1afd3eac064289fdbfb68365c838757", - "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx": "2c8750d9dbf0e70c6b39aff9b4c8f2797b1b738150a0c2741bd3cad854a6864c", - "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx": "bff8eddc760ff714cffc366ec0c4f7fb36a3d16250b11373d1dc05e724ea8ffd", - "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx": "4e81749f64327e2f0ce383f5d4f5065cdbb707ad4b719284e6d85c3e95d74c45", - "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx": "323f06f58b0f24aac4fb5fc17b46c464262dcb313923f22d38ba25822cf34bbe", - "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx": "e94c62f1bc8b944fb5937e0035d5db4940a4d3995faeb826d9f9cc46bc891ade", - "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx": "766fdb139db983be5c3630dbc361459eaebfc74c6e71ef4be4bce8516b5a084c", - "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx": "3763d567a22b5a5c3f3516529be7ed1ccf51108a8c19537a291de0c44f8471e0", - "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "eab67145c573d37fa1e91385b7deda97c3e4a97aa14e1eaf58aa56ea940f60a4", - "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx": "c40be5911f961215ed4ceb3e4838d6efa52684d1bda550068c2502bdfdb838ec", - "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx": "48117aa58a96ea3b0c1c46e624621b6dced88bf368cc9affcd675884037f65bf", - "frontend-modern/src/features/vmware/VsphereActivityTable.tsx": "2f613f2ae6ad4a299cec849d237f22af20bf1dd2a41bf6859dce70af464c1b5f", - "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx": "8d77ce4fb029efde83e71006703f0b6c74db18295ce0dfdeee1931a2c780f0b2", - "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx": "de897d462823484d2167785cafbf0e4f029a243fdc98f6ff00082e1b458adf3f", - "frontend-modern/src/features/vmware/VsphereHostsTable.tsx": "63891a9f1f8f798bfc91a0c917da207b8a077c362483d1ebe07d3709afeeeeaf", - "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "b667f8183ccb5a763d9af82b6b6dfc4b5a8db109cfdf4069e3383ca2d588a028" + "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx": "9512529c0e6b86ba4e1055c5009158d442bd8d4a84c2dd9f73822a053989886a", + "frontend-modern/src/components/Settings/Settings.tsx": "72682c86dd055761cbd68d4f57b684afe4d978a0b53748859d0721e711ce321a", + "frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx": "7f8ddc0c826b5abe90f40ac08ef3856d046510a7f72afe2f40f32ae72e547eba", + "frontend-modern/src/components/WhatsNewCard.tsx": "90543f19d61e9ba6d93251a7bcc9c179fa8d7ccb6ab3364e364d0ea13b2c31f0", + "frontend-modern/src/i18n/messages.de.ts": "f43d3c32d59f32178531e11a56e6014d384831871bb8cc3b472fca8e20a96840", + "frontend-modern/src/i18n/messages.es.ts": "928c7c7b34dd75ffd00639cb8b27ad81c0e6c8210db8ff41c234a26c5b9c9031", + "frontend-modern/src/i18n/messages.ts": "fef88e4096b8d49dc8e4a5b721804bb3b602dddef442827f0b315bec0d45a435", + "frontend-modern/src/utils/localStorage.ts": "19a532f55271b2fc696e3ba4ae6ba098e759f0f9df207015c803f7fcc0050bd6" }, "routes": [ - "/truenas/overview" + "/ (first-run setup wizard, fresh data dir)", + "/settings/system-general#usage-telemetry (mock data, existing-install localStorage baseline)", + "/docs/PRIVACY (shipped privacy document)" ], "viewports": [ { "width": 1280, - "height": 800 + "height": 900 }, { "width": 390, @@ -117,15 +39,21 @@ } ], "states": [ - "Populated TrueNAS Systems table at desktop and narrow widths with reduced motion", - "Collapsed static resource row without tabindex or disclosure aria, with one named native disclosure button", - "Expanded inline system detail controlled by the disclosure button after keyboard and pointer activation", - "Platform inline-completion search exposed as a native textbox without unsupported combobox popup semantics" + "Setup welcome step: Usage statistics card, purpose-first copy, never-sold statement, \"you choose on the next step\", no PULSE_TELEMETRY instruction", + "Setup security step (bootstrap validation and quick-setup responses intercepted by Playwright, no real token): Usage statistics toggle default on, description leads with the benefit and carries the never-sold statement", + "Setup security step: toggle off, aria-pressed false; completion reached after Create Account", + "Settings system-general with prior release baseline: no telemetry-payload-update-notice banner rendered", + "Settings Usage data and privacy panel: description leads with purpose, carries the never-sold statement, Preview payload rendered as primary Button, Reset ID secondary", + "Settings panel with payload preview open at 390px: no horizontal page overflow", + "Shipped /docs/PRIVACY renders the \"What it is not used for\" section and the Payload changes table", + "Locales: en verified live; de/es catalog copy covered by i18n and SetupWizard localization unit tests" ], "interactions": [ - "focused the named disclosure button and expanded it with Enter at desktop and narrow widths", - "collapsed the disclosure with Space and confirmed aria-expanded and aria-controls stayed on the button", - "expanded the same detail by clicking non-interactive row content while retaining whole-row pointer convenience", - "confirmed the controlled detail row remained visible and ran the axe WCAG A/AA scan in the expanded state" + "Fill bootstrap token field with a placeholder and click Verify bootstrap token (validation endpoint intercepted)", + "Click Usage statistics toggle on the security step", + "Click Create Account & Continue: observed POST /api/security/quick-setup then POST /api/system/settings/update {\"telemetryEnabled\":false}", + "Click Preview payload in Settings and read the rendered heartbeat JSON", + "Click Full details from the Settings panel and read the popup document", + "Resize to 390x844 and repeat the wizard, security step, and Settings panel checks" ] } diff --git a/frontend-modern/public/docs/PRIVACY.md b/frontend-modern/public/docs/PRIVACY.md index f1df8dcbd..f1fbc51b3 100644 --- a/frontend-modern/public/docs/PRIVACY.md +++ b/frontend-modern/public/docs/PRIVACY.md @@ -22,9 +22,12 @@ While mock/demo fixture mode is enabled, Pulse suppresses outbound telemetry ent #### How to disable +- During first-run setup, switch off **Usage statistics** on the admin-account step, or - **Settings → System → General → Outbound usage telemetry** (toggle off), or - Set the environment variable `PULSE_TELEMETRY=false` +The first startup ping is sent about two minutes after Pulse starts. The setup and Settings switches stop every later ping; setting `PULSE_TELEMETRY=false` before the first start prevents the first one as well. + #### How to inspect or rotate it - **Settings → System → General → Preview payload** shows the exact heartbeat JSON Pulse would send with the current runtime state. @@ -346,6 +349,16 @@ added. - No alert content, AI prompts, chat messages, tool names, tool inputs, tool outputs, command text, action output, or token values - No names, email addresses, account identifiers, or other intentionally identifying personal content +#### What it is not used for + +- It is not sold, licensed, or shared with anyone else. Pulse's maintainer is the only reader, and the only destination is Pulse's own license server. +- It is not used for advertising, marketing, or outreach of any kind. Nothing in it can address you. +- It is not linked to a Pulse account, license key, purchase, or email address. The license server never joins telemetry rows to those records. +- It is not used to single out an install. Reads are aggregate, and the install ID rotates every 30 days. +- It is not kept: rows are deleted after 90 days. + +If any of this ever changes, that is a change in kind under **Payload changes** above and comes with an in-app notice before it takes effect. + #### Install ID rotation The telemetry install ID is pseudonymous, is not tied to a Pulse account, and rotates automatically every 30 days. @@ -359,6 +372,29 @@ highest activation stage reached. This local state contains no user, account, resource, URL, or content identifiers. It exists so daily pings can report buckets instead of exporting a sequence of setup events. +#### Payload changes + +Every change to the payload bumps the schema version, is listed here with its date, and appears in the release notes of the first release that carries it. Pulse does not interrupt existing installations with an in-app notice for a new counter inside an already-disclosed category; **Preview payload** in Settings always shows the exact current contract. An in-app notice is reserved for a change in kind: a new identifier, a new class of data, or a change to retention or handling. + +| Schema | Date | Change | +|--------|------|--------| +| 17 | 2026-09-02 | Closed Patrol provider class, effective Patrol autonomy level, coarse 30-day Patrol token buckets, and per-outcome investigation counts | +| 16 | 2026-08-30 | Four content-free workload-history adoption counters, each counted at most once per browser session | +| 15 | 2026-08-29 | Notification destination HTTP 5xx failures separated from rejected HTTP 4xx responses | +| 14 | 2026-08-29 | Identity-free alert quality outcomes in closed severity, age, and resolution-time buckets, with tenant denominators | +| 13 | 2026-08-29 | Local UI/API service observation plus the immediately previous release observation | +| 12 | 2026-08-27 | Patrol-origin action funnel counters | +| 11 | 2026-08-24 | Node connection test attempt and failure counts | +| 10 | 2026-08-21 | Patrol runtime blocked cause, from a fixed category list | +| 9 | 2026-08-19 | Refusals with no machine reason code separated from refusals with an unrecognised code | +| 8 | 2026-08-13 | Agent-side pre-mutation refusals split into target-change, prerequisite, and invalid-contract categories | +| 7 | 2026-08-05 | `audit_reads_30d` replaces `audit_logging_persistent` and `audit_events_30d` | +| 6 | 2026-08-05 | Licensed-feature adoption counts; the never-populated Patrol autofix counter removed | +| 5 | 2026-07-29 | Bounded, content-free notification failure classes | +| 4 | 2026-07-27 | Complete approved-action outcome accounting, fixed pre-dispatch refusal categories, and verified finding-resolution linkage | +| 3 | 2026-07-23 | `notification_failures_7d` becomes a terminal-delivery count | +| 2 | 2026-07-23 | Coarse deployment, lifecycle, and estate-size buckets plus aggregate alert and notification outcome signals | + #### Source code The telemetry implementation is in [`internal/telemetry/telemetry.go`](../internal/telemetry/telemetry.go). You can read the `Ping` struct to see every field that is transmitted. diff --git a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx index d6f00707e..6176d13f1 100644 --- a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx @@ -285,7 +285,7 @@ export const GeneralSettingsPanel: Component = (props
+
+ setShareUsageStatistics(!shareUsageStatistics())} + label={t('setup.security.telemetry.title')} + description={t('setup.security.telemetry.description')} + /> +
+
{t('setup.security.nextScreen.title')} diff --git a/frontend-modern/src/components/WhatsNewCard.tsx b/frontend-modern/src/components/WhatsNewCard.tsx index 42b581865..4d90f41a9 100644 --- a/frontend-modern/src/components/WhatsNewCard.tsx +++ b/frontend-modern/src/components/WhatsNewCard.tsx @@ -1,7 +1,5 @@ import { Show, createEffect, createSignal } from 'solid-js'; -import { useNavigate } from '@solidjs/router'; import CheckCircleIcon from 'lucide-solid/icons/check-circle'; -import InfoIcon from 'lucide-solid/icons/info'; import XIcon from 'lucide-solid/icons/x'; import { updateStore } from '@/stores/updates'; import { UpdatesAPI } from '@/api/updates'; @@ -13,13 +11,8 @@ import { InlineNotice } from '@/components/shared/InlineNotice'; import { buildReleaseNotesUrl, normalizeReleaseVersion } from '@/components/updateVersion'; import { extractChangelog, isReleaseVersion } from '@/components/whatsNewModel'; import { renderMarkdown } from '@/components/AI/aiChatUtils'; -import { t } from '@/i18n'; -import { PRIVACY_DOC_URL } from '@/utils/docsLinks'; import { logger } from '@/utils/logger'; -const TELEMETRY_PAYLOAD_NOTICE_VERSION = '2'; -const TELEMETRY_SETTINGS_SECTION_ID = 'usage-telemetry'; - const readLastSeenVersion = (): string | null => { try { return localStorage.getItem(STORAGE_KEYS.WHATS_NEW_LAST_SEEN); @@ -36,25 +29,6 @@ const markVersionSeen = (version: string) => { } }; -const readTelemetryPayloadNoticeVersion = (): string | null => { - try { - return localStorage.getItem(STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN); - } catch { - return null; - } -}; - -const markTelemetryPayloadNoticeSeen = () => { - try { - localStorage.setItem( - STORAGE_KEYS.TELEMETRY_PAYLOAD_NOTICE_SEEN, - TELEMETRY_PAYLOAD_NOTICE_VERSION, - ); - } catch { - // Private mode / storage disabled: setup and Settings remain the fallback disclosure. - } -}; - /** * Post-update "What's New" notice. A compact non-blocking notice appears once * after the running version changes and only when that release has categorized @@ -62,26 +36,16 @@ const markTelemetryPayloadNoticeSeen = () => { * explicit action. Preparing the notice (or finding no categorized entries) * records the version so reloads stay quiet until the next update. * - * This release communication boundary also owns the one-time, non-blocking - * telemetry schema v2 notice. Existing installations see it once; fresh - * installs stay quiet because setup already presents the current disclosure. + * Telemetry payload changes are not announced here. They are disclosed in + * release notes and the dated "Payload changes" section of docs/PRIVACY.md; + * the Settings payload preview always shows the exact current contract. */ export function WhatsNewCard() { - const navigate = useNavigate(); const [noticeVisible, setNoticeVisible] = createSignal(false); const [dialogVisible, setDialogVisible] = createSignal(false); - const [telemetryNoticeVisible, setTelemetryNoticeVisible] = createSignal(false); const [version, setVersion] = createSignal(''); const [changelogHtml, setChangelogHtml] = createSignal(''); - const hadPriorReleaseBaseline = readLastSeenVersion() !== null; - const telemetryNoticeAlreadySeen = - readTelemetryPayloadNoticeVersion() === TELEMETRY_PAYLOAD_NOTICE_VERSION; - const telemetryNoticeNeedsSession = hadPriorReleaseBaseline && !telemetryNoticeAlreadySeen; - if (telemetryNoticeNeedsSession) { - reserveLowPriorityNoticeSession('telemetry-update'); - } let checked = false; - let telemetryNoticeChecked = false; const loadNotes = async (currentVersion: string, noticeSlotReserved: boolean) => { try { @@ -142,28 +106,6 @@ export function WhatsNewCard() { void loadNotes(currentVersion, noticeSlotReserved); }); - createEffect(() => { - const info = updateStore.versionInfo(); - if (!info || telemetryNoticeChecked) return; - telemetryNoticeChecked = true; - - if (info.isDevelopment || info.isSourceBuild || !isReleaseVersion(info.version)) { - return; - } - - if (!hadPriorReleaseBaseline) { - // Setup already showed the current telemetry disclosure on a fresh install. - markTelemetryPayloadNoticeSeen(); - return; - } - - if (readTelemetryPayloadNoticeVersion() === TELEMETRY_PAYLOAD_NOTICE_VERSION) { - return; - } - - setTelemetryNoticeVisible(true); - }); - const dismissNotice = () => { setNoticeVisible(false); }; @@ -177,63 +119,8 @@ export function WhatsNewCard() { setDialogVisible(false); }; - const dismissTelemetryNotice = () => { - markTelemetryPayloadNoticeSeen(); - setTelemetryNoticeVisible(false); - }; - - const openTelemetrySettings = (action: 'preview' | 'disable') => { - dismissTelemetryNotice(); - navigate(`/settings/system-general?telemetryAction=${action}#${TELEMETRY_SETTINGS_SECTION_ID}`); - }; - return ( <> - - - -