From 9b601f457c445c4f65725c5f437e44f58e90d455 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 28 Mar 2026 18:19:52 +0000 Subject: [PATCH] fix(frontend): ship telemetry disclosure docs locally --- .../subsystems/frontend-primitives.md | 9 ++ .../internal/subsystems/security-privacy.md | 50 ++++--- frontend-modern/public/docs/PRIVACY.md | 77 +++++++++++ frontend-modern/public/docs/README.md | 126 ++++++++++++++++++ .../scripts/build-embed-assets.mjs | 2 + frontend-modern/scripts/sync-public-docs.mjs | 33 +++++ .../Settings/GeneralSettingsPanel.tsx | 3 +- .../GeneralSettingsPanel.guardrails.test.ts | 11 ++ .../__tests__/settingsArchitecture.test.ts | 5 + .../shared/__tests__/WhatsNewModal.test.tsx | 4 + .../components/shared/whatsNewModalModel.ts | 7 +- .../src/utils/__tests__/docsLinks.test.ts | 37 +++++ frontend-modern/src/utils/docsLinks.ts | 8 ++ 13 files changed, 347 insertions(+), 25 deletions(-) create mode 100644 frontend-modern/public/docs/PRIVACY.md create mode 100644 frontend-modern/public/docs/README.md create mode 100644 frontend-modern/scripts/sync-public-docs.mjs create mode 100644 frontend-modern/src/components/Settings/__tests__/GeneralSettingsPanel.guardrails.test.ts create mode 100644 frontend-modern/src/utils/__tests__/docsLinks.test.ts create mode 100644 frontend-modern/src/utils/docsLinks.ts diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 540a9b761..61dab0f0d 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -125,6 +125,7 @@ work extends shared components instead of creating new local variants. 94. `frontend-modern/src/components/Settings/DockerRuntimeSettingsCard.tsx` 95. `frontend-modern/src/components/shared/EnvironmentLockBadge.tsx` 96. `frontend-modern/src/utils/environmentLockPresentation.ts` +97. `frontend-modern/src/utils/docsLinks.ts` ## Shared Boundaries @@ -680,6 +681,10 @@ product copy, or external links back into the shared shell. Internal product navigation from that shell should still route through canonical shared helpers such as `frontend-modern/src/routing/resourceLinks.ts` rather than freezing raw `/recovery?...` route strings into the modal itself. +Canonical customer disclosures inside those shared shells now route through +`frontend-modern/src/utils/docsLinks.ts`, so settings and what's-new privacy +links resolve to shipped `/docs/...` assets instead of hard-coded GitHub +`main` URLs that can drift from the running build. The shared summary strip primitives now follow that same owner split. `frontend-modern/src/components/shared/SummaryPanel.tsx` and `frontend-modern/src/components/shared/SummaryMetricCard.tsx` stay the render @@ -1403,6 +1408,10 @@ The security-facing settings panels within that shell now also follow an explicit shared boundary with `security-privacy` so shell framing stays here while auth posture, token controls, and privacy semantics remain governed as a trust surface instead of generic UX copy. +That shared shell boundary now also covers version-matched docs-link framing: +customer-facing privacy disclosures in shared settings surfaces must route +through `frontend-modern/src/utils/docsLinks.ts` rather than panel-local +external URLs. Single-surface settings pages that only render one canonical `SettingsPanel` must stay rooted directly at that panel instead of wrapping it in an extra diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 14c997cb7..b589182f1 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -25,27 +25,29 @@ token-management visibility, and privacy controls to operators. 1. `SECURITY.md` 2. `docs/PRIVACY.md` -3. `frontend-modern/src/api/security.ts` -4. `frontend-modern/src/components/Settings/APITokenManager.tsx` -5. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` -6. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` -7. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` -8. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` -9. `frontend-modern/src/components/Settings/QuickSecuritySetup.tsx` -10. `frontend-modern/src/components/Settings/SecurityPostureSummary.tsx` -11. `frontend-modern/src/components/Settings/SSOProviderTypeIcon.tsx` -12. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` -13. `frontend-modern/src/components/Settings/useSystemSettingsState.ts` -14. `frontend-modern/src/utils/apiTokenPresentation.ts` -15. `frontend-modern/src/utils/auditLogPresentation.ts` -16. `frontend-modern/src/utils/auditWebhookPresentation.ts` -17. `frontend-modern/src/utils/securityAuthPresentation.ts` -18. `frontend-modern/src/utils/securityScorePresentation.ts` -19. `internal/api/security.go` -20. `internal/api/security_tokens.go` -21. `internal/api/system_settings.go` -22. `internal/telemetry/telemetry.go` -23. `internal/api/router_routes_auth_security.go` +3. `frontend-modern/public/docs/PRIVACY.md` +4. `frontend-modern/src/utils/docsLinks.ts` +5. `frontend-modern/src/api/security.ts` +6. `frontend-modern/src/components/Settings/APITokenManager.tsx` +7. `frontend-modern/src/components/Settings/apiTokenManagerModel.ts` +8. `frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx` +9. `frontend-modern/src/components/Settings/SecurityAuthPanel.tsx` +10. `frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx` +11. `frontend-modern/src/components/Settings/QuickSecuritySetup.tsx` +12. `frontend-modern/src/components/Settings/SecurityPostureSummary.tsx` +13. `frontend-modern/src/components/Settings/SSOProviderTypeIcon.tsx` +14. `frontend-modern/src/components/Settings/useAPITokenManagerState.ts` +15. `frontend-modern/src/components/Settings/useSystemSettingsState.ts` +16. `frontend-modern/src/utils/apiTokenPresentation.ts` +17. `frontend-modern/src/utils/auditLogPresentation.ts` +18. `frontend-modern/src/utils/auditWebhookPresentation.ts` +19. `frontend-modern/src/utils/securityAuthPresentation.ts` +20. `frontend-modern/src/utils/securityScorePresentation.ts` +21. `internal/api/security.go` +22. `internal/api/security_tokens.go` +23. `internal/api/system_settings.go` +24. `internal/telemetry/telemetry.go` +25. `internal/api/router_routes_auth_security.go` ## Shared Boundaries @@ -117,6 +119,12 @@ That shared token-management boundary now also includes `frontend-modern/src/utils/apiTokenPresentation.ts`, so API-token load, generate, and revoke errors stay on one governed customer-facing wording path instead of drifting back into hook-local notification strings. +Telemetry/privacy disclosures now also route through the shipped frontend docs +boundary: `frontend-modern/src/utils/docsLinks.ts` is the canonical frontend +owner for privacy-document URLs, while `frontend-modern/public/docs/PRIVACY.md` +is the version-matched asset served by the running build. Privacy disclosures +must not drift back to GitHub `main` links that can describe a different +revision than the installed runtime. That same governed settings trust boundary now also includes `frontend-modern/src/components/Settings/QuickSecuritySetup.tsx`, `frontend-modern/src/components/Settings/SecurityPostureSummary.tsx`, diff --git a/frontend-modern/public/docs/PRIVACY.md b/frontend-modern/public/docs/PRIVACY.md new file mode 100644 index 000000000..138128dec --- /dev/null +++ b/frontend-modern/public/docs/PRIVACY.md @@ -0,0 +1,77 @@ +# Privacy + +Pulse is designed to run locally. By default, your monitoring data stays on your server. + +## Anonymous Telemetry + +Pulse includes anonymous telemetry that is **enabled by default**. It sends a lightweight ping on startup and once every 24 hours to help the developer understand how many active installations exist and which features are in use. + +No hostnames, credentials, IP addresses, or personally identifiable information is ever sent. See the full field list below. + +### How to disable + +- **Settings → System → General → Anonymous telemetry** (toggle off), or +- Set the environment variable `PULSE_TELEMETRY=false` + +### Exactly what is sent + +Every field is listed below — nothing else leaves your server: + +| Field | Example | Purpose | +|-------|---------|---------| +| Install ID | `a1b2c3d4-...` | Random UUID generated locally, not tied to any account | +| Version | `6.0.0` | Pulse version | +| Platform | `docker` or `binary` | Deployment method | +| OS | `linux` | Operating system | +| Arch | `amd64` | CPU architecture | +| Event | `startup` or `heartbeat` | Whether this is a startup or daily ping | +| PVE nodes | `3` | Number of Proxmox VE nodes connected | +| PBS instances | `1` | Number of Proxmox Backup Server instances | +| PMG instances | `0` | Number of Proxmox Mail Gateway instances | +| VMs | `25` | Total VM count | +| Containers | `12` | Total LXC container count | +| Docker hosts | `2` | Number of Docker hosts monitored | +| Kubernetes clusters | `0` | Number of Kubernetes clusters | +| AI enabled | `true`/`false` | Whether AI features are turned on | +| Active alerts | `4` | Number of active alerts | +| Relay enabled | `true`/`false` | Whether remote access is enabled | +| SSO enabled | `true`/`false` | Whether OIDC/SSO is configured | +| Multi-tenant | `true`/`false` | Whether multi-tenant mode is on | +| License tier | `free`, `pro`, etc. | Current license tier | +| API tokens | `3` | Number of API tokens configured | + +### What is NOT sent + +- No IP addresses are stored server-side +- No hostnames, node names, VM names, or any infrastructure identifiers +- No Proxmox credentials, API tokens, or passwords +- No alert content, AI prompts, or chat messages +- No personally identifiable information of any kind + +### 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. + +## No Third-Party Analytics + +- There is no third-party analytics SDK in the frontend. +- Telemetry pings go only to the Pulse license server (`license.pulserelay.pro`), not to any third-party service. + +## Optional Outbound Connections (Explicitly Enabled) + +Pulse can make outbound connections when you enable specific features: + +- **AI (BYOK)**: when AI features are enabled, Pulse sends only the context required for your request to the provider you configured (OpenAI, Anthropic, etc.). See `docs/AI.md`. +- **Relay / Remote Access**: when relay is enabled, Pulse connects to the configured relay endpoint to enable mobile access. See Settings → Remote Access. +- **Update checks**: Pulse can check for new releases/updates (for example via GitHub release metadata) depending on your deployment and configuration. + +## Local Upgrade Metrics (Can Be Disabled) + +Pulse can record local-only events such as "paywall viewed" or "trial started" to improve and debug in-app upgrade flows. + +- These events are stored locally and are not exported to third parties. +- Disable via **Settings → System → General → Disable local upgrade metrics** or set: + - `PULSE_DISABLE_LOCAL_UPGRADE_METRICS=true` + +If you prefer fewer upgrade prompts, you can also enable: +- **Settings → System → General → Reduce Pro prompts** diff --git a/frontend-modern/public/docs/README.md b/frontend-modern/public/docs/README.md new file mode 100644 index 000000000..f46d94894 --- /dev/null +++ b/frontend-modern/public/docs/README.md @@ -0,0 +1,126 @@ +# 📚 Pulse Documentation + +Welcome to the Pulse documentation portal. Here you'll find everything you need to install, configure, and master Pulse. + +--- + +## v6 Execution Canonical Source + +For Pulse v6 build/release execution work, do not start from this broad docs index. +Use: + +1. `docs/release-control/v6/internal/SOURCE_OF_TRUTH.md` for stable human governance and locked decisions +2. `docs/release-control/v6/internal/status.json` for live lane state, lane-to-subsystem ownership, structured evidence references, typed lane/subsystem decision records, and canonical ordered lists +3. `docs/release-control/v6/status.schema.json` for the machine-readable status contract +4. `docs/release-control/v6/internal/subsystems/registry.json` and `docs/release-control/v6/internal/subsystems/registry.schema.json` for subsystem ownership, explicit shared-ownership exceptions, and proof-routing rules +5. `python3 scripts/release_control/status_audit.py --check` if you need a machine-derived evidence health audit +6. `python3 scripts/release_control/registry_audit.py --check` if you need a machine-derived subsystem registry audit +7. `python3 scripts/release_control/contract_audit.py --check` if you need a machine-derived subsystem contract audit, including explicit cross-subsystem dependency checks and exact registry-derived shared-boundary wording + Local pre-commit runs the v6 machine audits against staged control-file content so partial staging cannot hide governance drift. + Local pre-commit also blocks partial staging for hook-sensitive governance files under `docs/release-control/v6/`, `scripts/release_control/`, `internal/repoctl/`, `.husky/pre-commit`, and `.github/workflows/canonical-governance.yml`, because those checks still execute or structurally read the working-tree versions locally. +8. `python3 scripts/release_control/subsystem_lookup.py [ ...]` if you need subsystem ownership, proof routing, lane context, relevant decision records, and dependent contract-update obligations for a change + +For governed runtime changes, a staged subsystem contract only counts if its +diff updates a substantive contract section such as `Purpose`, `Canonical Files`, +`Shared Boundaries`, `Extension Points`, `Forbidden Paths`, +`Completion Obligations`, or `Current State`, rather than metadata alone. + +All other documents are supporting references unless explicitly required for evidence. + +--- + +## 🚀 Getting Started + +- **[Installation Guide](INSTALL.md)** + Step-by-step guides for Docker, Kubernetes, and bare metal. +- **[Configuration](CONFIGURATION.md)** + Learn how to configure authentication, notifications (Email, Discord, etc.), and system settings. +- **[Deployment Models](DEPLOYMENT_MODELS.md)** + Where config lives, how updates work, and what differs per deployment. +- **[Migration Guide](MIGRATION.md)** + Moving to a new server? Here's how to export and import your data safely. +- **[Upgrade to v6](UPGRADE_v6.md)** + Practical upgrade guidance and post-upgrade checks for Pulse v6. +- **[FAQ](FAQ.md)** + Common questions and quick answers. + +## 🛠️ Deployment & Operations + +- **[Docker Guide](DOCKER.md)** – Advanced Docker & Compose configurations. +- **[Kubernetes](KUBERNETES.md)** – Helm charts, ingress, and HA setups. +- **[Reverse Proxy](REVERSE_PROXY.md)** – Nginx, Caddy, Traefik, and Cloudflare Tunnel recipes. +- **[Troubleshooting](TROUBLESHOOTING.md)** – Deep dive into common issues and logs. + +## 🔐 Security + +- **[Security Policy](../SECURITY.md)** – The core security model (Encryption, Auth, API Scopes). +- **[Privacy](PRIVACY.md)** – What leaves your network (and what doesn’t). +- **[OIDC / SSO](OIDC.md)** – OIDC Single Sign-On configuration (Authentik, Keycloak, Azure AD, etc.). +- **[Proxy Auth](PROXY_AUTH.md)** – Authentik/Authelia/Cloudflare proxy authentication configuration. +- **[Agent Security](AGENT_SECURITY.md)** – Agent self-update verification and API security. + +## 📖 Advanced Topics (Relay / Pro / Pro+ / Cloud) + +- **[AI Autonomy & Safety](AI_AUTONOMY.md)** – Configure patrol autonomy levels, assistant control levels, investigation tuning, and safety guardrails. +- **[Role-Based Access Control (RBAC)](RBAC.md)** – Define custom roles, assign permissions, and integrate with OIDC group mapping. +- **[Audit Logging](AUDIT_LOGGING.md)** – Tamper-evident event logging for compliance, with query, export, and signature verification. + +## ✨ New in 6.0 + +- **[Unified Resource Model](UNIFIED_RESOURCES.md)** – How all platforms merge into one model with task-based navigation. +- **[Unified Navigation Migration](MIGRATION_UNIFIED_NAV.md)** – Upgrading from platform-specific tabs to v6 navigation. +- **[TrueNAS Integration](TRUENAS.md)** – First-class TrueNAS SCALE/CORE monitoring (pools, datasets, disks, snapshots, replication). +- **[Relay / Mobile Remote Access](RELAY.md)** – End-to-end encrypted relay (mobile app public rollout is coming soon; Relay and above). +- **[Recovery Central](RECOVERY.md)** – Unified backup, snapshot, and replication view across all providers. +- **[Pulse Cloud (Hosted)](CLOUD.md)** – Fully managed hosting with automatic updates and backups. +- **[Pulse AI](AI.md)** – Chat assistant, patrol findings, alert analysis, intelligence, and forecasts. +- **[Metrics History](METRICS_HISTORY.md)** – Persistent metrics storage with configurable retention. +- **[Mail Gateway](MAIL_GATEWAY.md)** – Proxmox Mail Gateway (PMG) monitoring. +- **[Auto Updates](AUTO_UPDATE.md)** – One-click updates for supported deployments. +- **[Multi-Tenant Organizations](MULTI_TENANT.md)** – Isolate infrastructure by organization (Enterprise, opt-in). +- **[Entitlements Overhaul](PULSE_PRO.md)** – Capability-key-based feature gating across Community/Relay/Pro/Pro+/Cloud. + +## 💳 Plans (Community / Relay / Pro / Pro+ / Cloud) + +Pulse is available in four self-hosted tiers plus hosted Cloud: + +- **Community**: Free self-hosted monitoring for up to 5 monitored systems with 7-day history. +- **Relay**: Adds remote access, mobile, push notifications, 14-day history, and raises the monitored-system limit to 8. +- **Pro**: Adds AI investigation, auto-fix, operations tooling, and raises the monitored-system limit to 15 with 90-day history. +- **Pro+**: Everything in Pro with room for up to 50 monitored systems. +- **Cloud**: Hosted Pulse with Pro-level capabilities; hosted pricing is unchanged by the self-hosted model lock. + +- **[Learn more at pulserelay.pro](https://pulserelay.pro)** +- **[Plans and entitlements](PULSE_PRO.md)** (includes the Community/Relay/Pro/Pro+/Cloud matrix) +- **[AI deep dive](AI.md)** +- **[Multi-Tenant Organizations (Enterprise)](MULTI_TENANT.md)** — Isolate infrastructure by organization for MSPs and multi-datacenter deployments. + +## 📡 Monitoring & Agents + +- **[Unified Agent](UNIFIED_AGENT.md)** – Single binary for host, Docker, and Kubernetes monitoring. +- **[Centralized Agent Management (Pro/Pro+/Cloud)](CENTRALIZED_MANAGEMENT.md)** – Agent profiles and remote config. +- **[Proxmox Backup Server](PBS.md)** – PBS integration, direct API vs PVE passthrough, token setup. +- **[TrueNAS](TRUENAS.md)** – TrueNAS SCALE/CORE integration. +- **[ZFS Monitoring](ZFS_MONITORING.md)** – Proxmox-native ZFS pool monitoring. +- **[Storage Architecture](STORAGE_ARCHITECTURE.md)** – Proposed canonical storage, disk, S.M.A.R.T., and topology model for making storage genuinely operator-useful. +- **[VM Disk Monitoring](VM_DISK_MONITORING.md)** – Enabling QEMU Guest Agent for disk stats. +- **[Temperature Monitoring](TEMPERATURE_MONITORING.md)** – Agent-based temperature monitoring (`pulse-agent --enable-proxmox`). Sensor proxy has been removed. +- **[Webhooks](WEBHOOKS.md)** – Custom notification payloads. + +## 💻 Development + +- **[API Reference](API.md)** – Complete REST API documentation. +- **[Architecture](../ARCHITECTURE.md)** – System design and component interaction. +- **[Contributing](../CONTRIBUTING.md)** – How to contribute to Pulse. + +## 📁 Previous Versions + +- **[Upgrade to v5](UPGRADE_v5.md)** – Upgrade guidance for v4 → v5 migrations. +- **[v6 Release Promotion Policy](release-control/v6/internal/RELEASE_PROMOTION_POLICY.md)** – Canonical stable-vs-prerelease promotion rules and rollback expectations. +- **[v6 Prerelease Runbook](releases/V6_PRERELEASE_RUNBOOK.md)** – Internal release operations used during the v6 prerelease period. + +--- + +Found a bug or have a suggestion? + +[![GitHub Issues](https://img.shields.io/badge/GitHub-Issues-green)](https://github.com/rcourtman/Pulse/issues) diff --git a/frontend-modern/scripts/build-embed-assets.mjs b/frontend-modern/scripts/build-embed-assets.mjs index 4096b3504..84663ba14 100644 --- a/frontend-modern/scripts/build-embed-assets.mjs +++ b/frontend-modern/scripts/build-embed-assets.mjs @@ -4,6 +4,7 @@ import { fileURLToPath } from 'node:url'; import { withExclusiveLock } from '../../scripts/exclusive-lock.mjs'; import { syncEmbedDir } from './sync-embed-dist.mjs'; +import { syncPublicDocs } from './sync-public-docs.mjs'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -29,6 +30,7 @@ function run(command, args, options = {}) { await withExclusiveLock( lockPath, async () => { + await syncPublicDocs(); await run(npxCmd, ['vite', 'build'], { cwd: frontendRoot }); await syncEmbedDir({ lock: false }); }, diff --git a/frontend-modern/scripts/sync-public-docs.mjs b/frontend-modern/scripts/sync-public-docs.mjs new file mode 100644 index 000000000..26a78758f --- /dev/null +++ b/frontend-modern/scripts/sync-public-docs.mjs @@ -0,0 +1,33 @@ +import { copyFile, mkdir } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const frontendRoot = path.resolve(__dirname, '..'); +const repoRoot = path.resolve(frontendRoot, '..'); +const sourceDocsDir = path.join(repoRoot, 'docs'); +const targetDocsDir = path.join(frontendRoot, 'public', 'docs'); + +const shippedDocs = ['README.md', 'PRIVACY.md']; + +export async function syncPublicDocs() { + await mkdir(targetDocsDir, { recursive: true }); + + for (const filename of shippedDocs) { + await copyFile(path.join(sourceDocsDir, filename), path.join(targetDocsDir, filename)); + } + + console.log(`Synced shipped docs to ${targetDocsDir}`); +} + +const isMainModule = process.argv[1] + ? pathToFileURL(process.argv[1]).href === import.meta.url + : false; + +if (isMainModule) { + syncPublicDocs().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + }); +} diff --git a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx index b52a76575..ce576e2c4 100644 --- a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx @@ -17,6 +17,7 @@ import { PVE_POLLING_MIN_SECONDS, PVE_POLLING_PRESETS, } from '@/utils/systemSettingsPresentation'; +import { PRIVACY_DOC_URL } from '@/utils/docsLinks'; import Laptop from 'lucide-solid/icons/laptop'; @@ -214,7 +215,7 @@ export const GeneralSettingsPanel: Component = (props platform, resource counts, and feature flags. No hostnames, credentials, or personal information is ever sent.{' '} { + it('routes telemetry disclosure through the shipped privacy doc URL', () => { + expect(generalSettingsPanelSource).toContain('PRIVACY_DOC_URL'); + expect(generalSettingsPanelSource).not.toContain( + 'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md', + ); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 24fcbb7fc..659387b5b 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -58,6 +58,7 @@ import billingAdminOrganizationsTableSource from '../BillingAdminOrganizationsTa import billingAdminPanelSource from '../BillingAdminPanel.tsx?raw'; import billingAdminPanelStateSource from '../useBillingAdminPanelState.ts?raw'; import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw'; +import docsLinksSource from '@/utils/docsLinks.ts?raw'; import aiSettingsPanelSource from '../AISettings.tsx?raw'; import aiChatMaintenanceSectionSource from '../AIChatMaintenanceSection.tsx?raw'; import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSection.tsx?raw'; @@ -1247,6 +1248,8 @@ describe('Settings architecture guardrails', () => { it('keeps shared system settings presentation extracted from panel and state owners', () => { expect(generalSettingsPanelSource).toContain('@/utils/systemSettingsPresentation'); + expect(generalSettingsPanelSource).toContain('@/utils/docsLinks'); + expect(generalSettingsPanelSource).toContain('PRIVACY_DOC_URL'); expect(recoverySettingsPanelSource).toContain('@/utils/systemSettingsPresentation'); expect(networkDiscoverySectionSource).toContain('@/utils/systemSettingsPresentation'); expect(systemSettingsStateSource).toContain('@/utils/systemSettingsPresentation'); @@ -1257,6 +1260,8 @@ describe('Settings architecture guardrails', () => { expect(systemSettingsPresentationSource).toContain( 'export function getStartUpdateErrorMessage', ); + expect(docsLinksSource).toContain("export const SHIPPED_DOCS_ROOT = '/docs'"); + expect(docsLinksSource).toContain("export const PRIVACY_DOC_URL = getShippedDocUrl('PRIVACY.md')"); }); it('routes every top-level settings surface through the canonical panel shell framing', () => { diff --git a/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx b/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx index 7415ce68a..d94049cc1 100644 --- a/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/WhatsNewModal.test.tsx @@ -37,6 +37,10 @@ describe('WhatsNewModal', () => { expect(whatsNewModalModelSource).toContain('WHATS_NEW_TELEMETRY_TITLE'); expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_URL'); expect(whatsNewModalModelSource).toContain('WHATS_NEW_PRIVACY_URL'); + expect(whatsNewModalModelSource).toContain('README_DOC_URL'); + expect(whatsNewModalModelSource).toContain('PRIVACY_DOC_URL'); + expect(whatsNewModalModelSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/README.md'); + expect(whatsNewModalModelSource).not.toContain('https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md'); expect(whatsNewModalModelSource).toContain('WHATS_NEW_DOCS_LABEL'); expect(whatsNewModalModelSource).toContain("title: 'Infrastructure'"); }); diff --git a/frontend-modern/src/components/shared/whatsNewModalModel.ts b/frontend-modern/src/components/shared/whatsNewModalModel.ts index e3fe3272b..d167891dc 100644 --- a/frontend-modern/src/components/shared/whatsNewModalModel.ts +++ b/frontend-modern/src/components/shared/whatsNewModalModel.ts @@ -1,3 +1,5 @@ +import { PRIVACY_DOC_URL, README_DOC_URL } from '@/utils/docsLinks'; + export interface WhatsNewFeatureCard { accent: string; description: string; @@ -5,9 +7,8 @@ export interface WhatsNewFeatureCard { title: string; } -export const WHATS_NEW_DOCS_URL = 'https://github.com/rcourtman/Pulse/blob/main/docs/README.md'; -export const WHATS_NEW_PRIVACY_URL = - 'https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md'; +export const WHATS_NEW_DOCS_URL = README_DOC_URL; +export const WHATS_NEW_PRIVACY_URL = PRIVACY_DOC_URL; export const WHATS_NEW_FEATURE_CARDS: WhatsNewFeatureCard[] = [ { diff --git a/frontend-modern/src/utils/__tests__/docsLinks.test.ts b/frontend-modern/src/utils/__tests__/docsLinks.test.ts new file mode 100644 index 000000000..9437c1095 --- /dev/null +++ b/frontend-modern/src/utils/__tests__/docsLinks.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + PRIVACY_DOC_URL, + README_DOC_URL, + SHIPPED_DOCS_ROOT, + getShippedDocUrl, +} from '@/utils/docsLinks'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const frontendRoot = path.resolve(__dirname, '..', '..', '..'); +const repoRoot = path.resolve(frontendRoot, '..'); + +describe('docsLinks', () => { + it('returns canonical shipped doc URLs', () => { + expect(SHIPPED_DOCS_ROOT).toBe('/docs'); + expect(getShippedDocUrl('PRIVACY.md')).toBe('/docs/PRIVACY.md'); + expect(PRIVACY_DOC_URL).toBe('/docs/PRIVACY.md'); + expect(README_DOC_URL).toBe('/docs/README.md'); + }); + + it('keeps shipped privacy and docs content synced with repo docs', () => { + const rootPrivacy = readFileSync(path.join(repoRoot, 'docs', 'PRIVACY.md'), 'utf8'); + const publicPrivacy = readFileSync( + path.join(frontendRoot, 'public', 'docs', 'PRIVACY.md'), + 'utf8', + ); + const rootReadme = readFileSync(path.join(repoRoot, 'docs', 'README.md'), 'utf8'); + const publicReadme = readFileSync(path.join(frontendRoot, 'public', 'docs', 'README.md'), 'utf8'); + + expect(publicPrivacy).toBe(rootPrivacy); + expect(publicReadme).toBe(rootReadme); + }); +}); diff --git a/frontend-modern/src/utils/docsLinks.ts b/frontend-modern/src/utils/docsLinks.ts new file mode 100644 index 000000000..97f908425 --- /dev/null +++ b/frontend-modern/src/utils/docsLinks.ts @@ -0,0 +1,8 @@ +export const SHIPPED_DOCS_ROOT = '/docs'; + +export function getShippedDocUrl(filename: string): string { + return `${SHIPPED_DOCS_ROOT}/${filename}`; +} + +export const README_DOC_URL = getShippedDocUrl('README.md'); +export const PRIVACY_DOC_URL = getShippedDocUrl('PRIVACY.md');