fix(frontend): ship telemetry disclosure docs locally

This commit is contained in:
rcourtman
2026-03-28 18:19:52 +00:00
parent 8eea329ef6
commit 9b601f457c
13 changed files with 347 additions and 25 deletions
@@ -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
@@ -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`,
+77
View File
@@ -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**
+126
View File
@@ -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 <path> [<path> ...]` 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 doesnt).
- **[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)
@@ -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 });
},
@@ -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);
});
}
@@ -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<GeneralSettingsPanelProps> = (props
platform, resource counts, and feature flags. No hostnames, credentials, or personal
information is ever sent.{' '}
<a
href="https://github.com/rcourtman/Pulse/blob/main/docs/PRIVACY.md"
href={PRIVACY_DOC_URL}
target="_blank"
rel="noopener noreferrer"
class="underline hover:text-base-content"
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest';
import generalSettingsPanelSource from '@/components/Settings/GeneralSettingsPanel.tsx?raw';
describe('GeneralSettingsPanel guardrails', () => {
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',
);
});
});
@@ -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', () => {
@@ -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'");
});
@@ -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[] = [
{
@@ -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);
});
});
+8
View File
@@ -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');