From 1c999113a18505c869a92c8c1c2b0249cb640254 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:28:45 +0100 Subject: [PATCH 1/2] Separate TrueNAS setup acceptance from connection probes Remove the one-cycle data guarantee at the first setup steps, explain which ordinary reads a successful system-information probe has not exercised, and retain the published 6.4.1/6.4.5 probe-timestamp warning without applying it to repaired builds. Preserve read-only, credential-free polling checks and verify their shipped rendering and keyboard path. Contract-Neutral: Existing help and its verification only; no runtime, API, polling, access, persistence or retention change. Change-source: pulse-maintainer --- docs/TRUENAS.md | 29 ++++--- .../docs-fragment-navigation.tsx | 1 + .../browser-tests/truenas-connection-help.cjs | 75 +++++++++++++++++++ frontend-modern/public/docs/TRUENAS.md | 29 ++++--- .../src/utils/__tests__/docsLinks.test.ts | 18 +++++ 5 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 frontend-modern/browser-tests/truenas-connection-help.cjs diff --git a/docs/TRUENAS.md b/docs/TRUENAS.md index 54dac2b6e6..270d15e838 100644 --- a/docs/TRUENAS.md +++ b/docs/TRUENAS.md @@ -9,8 +9,13 @@ Pulse v6 includes first-class monitoring for **TrueNAS SCALE** and **TrueNAS COR are listed under **Platform connections**. 3. Enter the TrueNAS URL (e.g., `https://truenas.local`), the API key, and the username that owns the key. -4. Click **Test Connection** → **Save**. -5. Data appears within one configured polling cycle (60 seconds by default). +4. Click **Test Connection**, then **Save**. The test reads system information + on a separate connection; it does not validate inventory or metric collection. +5. Leave the saved connection enabled and allow at least one configured polling + interval (60 seconds by default). Check that the appliance and expected + inventory appear. If they do not, follow the [polling checks](#stale-truenas-data) + without repeatedly testing or restarting; an elapsed interval is not proof + that a poll completed. ## Creating a TrueNAS API Key @@ -155,9 +160,11 @@ its trusted HTTPS origin without disabling certificate verification. ### No data appearing after adding connection - Allow one configured polling cycle (60 seconds by default), not a fixed 30-second wait. Check that the saved connection is enabled. -- **Test Connection** checks authentication and the selected transport, not - successful collection of every metric. The legacy REST diagnostic is - expected for recognized CORE 13 systems; it is not itself a connection error. +- **Test Connection** reads system information on a separate connection. Pools, + datasets, disks and alerts can still fail during ordinary polling, even when + the test succeeds. A successful test also does not establish live CPU, memory + or History readings. The legacy REST diagnostic is expected for recognized + CORE 13 systems; it is not itself a connection error. - Inspect a bounded local log excerpt for TrueNAS-related errors: ```bash journalctl -u pulse -n 100 --no-pager | grep -i truenas @@ -236,11 +243,13 @@ collection step. | No timestamps advance, or no completed attempt is recorded | Check that the connection and integration are enabled, then preserve the bounded local error above. This is not proof of an invalid key. | | Reconnects or TrueNAS sign-ins increase while `observed.collectedAt` advances | Session turnover alone does not establish failed authentication or stopped polling. Record the timing and any existing close/error message separately from the data freshness. | -**Test Connection** checks connectivity and authentication, not a complete -poll. Testing a saved connection can update `lastAttemptAt` and `lastSuccessAt` -and reset its failure count without refreshing `observed.collectedAt`. Record -any manual test and its time separately; a successful test is not evidence -that missing data or stopped polling has recovered. +**Test Connection** is not a complete poll. Some older builds, including Pulse +6.4.1 and 6.4.5, also let a saved-connection test update `lastAttemptAt` and +`lastSuccessAt` and reset its failure count without refreshing +`observed.collectedAt`. Record any manual test and its time separately, and +use the inventory observation time rather than the test time to check freshness. +A successful test is not evidence that missing data or stopped polling has +recovered. An `app.stats` error such as **Apps are not available** concerns Apps collection, not necessarily the key. Check the existing Apps status in diff --git a/frontend-modern/browser-tests/docs-fragment-navigation.tsx b/frontend-modern/browser-tests/docs-fragment-navigation.tsx index 5c11587d77..2c125f2a3b 100644 --- a/frontend-modern/browser-tests/docs-fragment-navigation.tsx +++ b/frontend-modern/browser-tests/docs-fragment-navigation.tsx @@ -22,6 +22,7 @@ const targets: Record = { malformed: '/docs/API#%invalid', missing: '/docs/API#does-not-exist', troubleshooting: '/docs/TROUBLESHOOTING#recovery-mode', + truenas: '/docs/TRUENAS', }; window.history.replaceState({}, '', targets[scenario] ?? targets.plain); diff --git a/frontend-modern/browser-tests/truenas-connection-help.cjs b/frontend-modern/browser-tests/truenas-connection-help.cjs new file mode 100644 index 0000000000..17a7fcfae9 --- /dev/null +++ b/frontend-modern/browser-tests/truenas-connection-help.cjs @@ -0,0 +1,75 @@ +// The real Docs page/router/styles and shipped TrueNAS help, without a backend. +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { chromium, webkit } = require('playwright'); + +(async () => { + const root = '/workspace/frontend-modern'; + const artifacts = path.join(root, 'node_modules', 'truenas-connection-help-browser'); + fs.mkdirSync(artifacts, { recursive: true }); + const { createServer } = await import(path.join(root, 'node_modules/vite/dist/node/index.js')); + const server = await createServer({ + root, + configFile: path.join(root, 'vite.config.ts'), + cacheDir: path.join(root, 'node_modules', '.vite-truenas-connection-help'), + server: { host: '127.0.0.1', port: 5250, strictPort: true }, + optimizeDeps: { noDiscovery: true, include: [] }, + }); + const results = []; + let browser; + try { + await server.listen(); + for (const [engine, width] of [['chromium', 1280], ['webkit', 390]]) { + browser = await { chromium, webkit }[engine].launch(engine === 'chromium' + ? { headless: true, channel: 'chromium', args: ['--no-sandbox'] } + : { headless: true }); + const page = await browser.newPage({ viewport: { width, height: 900 }, + ...(engine === 'webkit' ? { isMobile: true, hasTouch: true } : {}) }); + const errors = []; + page.on('pageerror', (error) => errors.push(error.message)); + page.on('console', (message) => { + if (message.type() === 'error') errors.push(message.text()); + }); + await page.goto('http://127.0.0.1:5250/browser-tests/docs-fragment-navigation.html?scenario=truenas', + { waitUntil: 'domcontentloaded', timeout: 60_000 }); + const quickStart = page.getByRole('heading', { name: 'Quick Start', exact: true }); + await quickStart.waitFor(); + if (engine === 'webkit') await page.evaluate(() => document.documentElement.classList.add('dark')); + const article = await page.locator('article').innerText(); + for (const text of ['it does not validate inventory or metric collection', + 'an elapsed interval is not proof that a poll completed', + 'Some older builds, including Pulse 6.4.1 and 6.4.5', + 'use the inventory observation time rather than the test time', + 'do not copy a token or cookie', 'Do not post the full connection response']) + assert.ok(article.includes(text), text); + assert.ok(!article.includes('Data appears within one configured polling cycle')); + await quickStart.evaluate((heading) => heading.scrollIntoView({ block: 'start' })); + await page.screenshot({ path: path.join(artifacts, `${engine}-setup.png`) }); + const checks = page.getByRole('link', { name: 'polling checks', exact: true }).first(); + await checks.focus(); + await page.keyboard.press('Enter'); + await page.waitForFunction(() => { + const heading = document.getElementById('stale-truenas-data'); + return heading && heading.getBoundingClientRect().top >= 0 + && heading.getBoundingClientRect().top < innerHeight; + }); + assert.ok(page.url().endsWith('/docs/TRUENAS#stale-truenas-data')); + assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1)); + await page.screenshot({ path: path.join(artifacts, `${engine}-polling.png`) }); + assert.deepEqual(errors, []); + results.push({ engine, version: browser.version(), width, + probeScope: true, noGuaranteedPoll: true, versionQualifiedWarning: true, + keyboardPollingLink: true, noDocumentOverflow: true, errors }); + await browser.close(); + browser = undefined; + } + const result = { result: 'passed', playwright: require('playwright/package.json').version, + scope: 'shipped help rendering/navigation, not native TrueNAS polling', results }; + fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2) + '\n'); + console.log(JSON.stringify(result)); + } finally { + if (browser) await browser.close(); + await server.close(); + } +})().catch((error) => { console.error(error); process.exitCode = 1; }); diff --git a/frontend-modern/public/docs/TRUENAS.md b/frontend-modern/public/docs/TRUENAS.md index 54dac2b6e6..270d15e838 100644 --- a/frontend-modern/public/docs/TRUENAS.md +++ b/frontend-modern/public/docs/TRUENAS.md @@ -9,8 +9,13 @@ Pulse v6 includes first-class monitoring for **TrueNAS SCALE** and **TrueNAS COR are listed under **Platform connections**. 3. Enter the TrueNAS URL (e.g., `https://truenas.local`), the API key, and the username that owns the key. -4. Click **Test Connection** → **Save**. -5. Data appears within one configured polling cycle (60 seconds by default). +4. Click **Test Connection**, then **Save**. The test reads system information + on a separate connection; it does not validate inventory or metric collection. +5. Leave the saved connection enabled and allow at least one configured polling + interval (60 seconds by default). Check that the appliance and expected + inventory appear. If they do not, follow the [polling checks](#stale-truenas-data) + without repeatedly testing or restarting; an elapsed interval is not proof + that a poll completed. ## Creating a TrueNAS API Key @@ -155,9 +160,11 @@ its trusted HTTPS origin without disabling certificate verification. ### No data appearing after adding connection - Allow one configured polling cycle (60 seconds by default), not a fixed 30-second wait. Check that the saved connection is enabled. -- **Test Connection** checks authentication and the selected transport, not - successful collection of every metric. The legacy REST diagnostic is - expected for recognized CORE 13 systems; it is not itself a connection error. +- **Test Connection** reads system information on a separate connection. Pools, + datasets, disks and alerts can still fail during ordinary polling, even when + the test succeeds. A successful test also does not establish live CPU, memory + or History readings. The legacy REST diagnostic is expected for recognized + CORE 13 systems; it is not itself a connection error. - Inspect a bounded local log excerpt for TrueNAS-related errors: ```bash journalctl -u pulse -n 100 --no-pager | grep -i truenas @@ -236,11 +243,13 @@ collection step. | No timestamps advance, or no completed attempt is recorded | Check that the connection and integration are enabled, then preserve the bounded local error above. This is not proof of an invalid key. | | Reconnects or TrueNAS sign-ins increase while `observed.collectedAt` advances | Session turnover alone does not establish failed authentication or stopped polling. Record the timing and any existing close/error message separately from the data freshness. | -**Test Connection** checks connectivity and authentication, not a complete -poll. Testing a saved connection can update `lastAttemptAt` and `lastSuccessAt` -and reset its failure count without refreshing `observed.collectedAt`. Record -any manual test and its time separately; a successful test is not evidence -that missing data or stopped polling has recovered. +**Test Connection** is not a complete poll. Some older builds, including Pulse +6.4.1 and 6.4.5, also let a saved-connection test update `lastAttemptAt` and +`lastSuccessAt` and reset its failure count without refreshing +`observed.collectedAt`. Record any manual test and its time separately, and +use the inventory observation time rather than the test time to check freshness. +A successful test is not evidence that missing data or stopped polling has +recovered. An `app.stats` error such as **Apps are not available** concerns Apps collection, not necessarily the key. Check the existing Apps status in diff --git a/frontend-modern/src/utils/__tests__/docsLinks.test.ts b/frontend-modern/src/utils/__tests__/docsLinks.test.ts index bfe282cb05..69144e2f20 100644 --- a/frontend-modern/src/utils/__tests__/docsLinks.test.ts +++ b/frontend-modern/src/utils/__tests__/docsLinks.test.ts @@ -61,6 +61,24 @@ function getRuntimeSourceFiles(dir: string): string[] { } describe('docsLinks', () => { + it('keeps TrueNAS setup acceptance separate from a system-information probe', () => { + const truenas = readFileSync(path.join(repoRoot, 'docs', 'TRUENAS.md'), 'utf8'); + const setup = truenas.split('## Quick Start')[1].split('## Creating a TrueNAS API Key')[0]; + expect(setup).toContain('it does not validate inventory or metric collection'); + expect(setup).toContain('an elapsed interval is not proof'); + expect(setup).toContain('[polling checks](#stale-truenas-data)'); + expect(setup).not.toContain('Data appears within one configured polling cycle'); + const noData = truenas + .split('### No data appearing after adding connection')[1] + .split('### Inventory works')[0]; + expect(noData).toContain('Pools,\n datasets, disks and alerts can still fail'); + expect(noData).toContain('does not establish live CPU, memory'); + expect(truenas).toContain('Some older builds, including Pulse\n6.4.1 and 6.4.5'); + expect(truenas).toContain('use the inventory observation time rather than the test time'); + expect(truenas).toContain('do not copy a token or cookie'); + expect(truenas).toContain('Do not post the full connection response'); + }); + it('keeps restored Proxmox node network details in the candidate packet', () => { const releaseNotes = readFileSync( path.join(repoRoot, 'docs', 'releases', 'RELEASE_NOTES_v6.4.0-rc.1.md'), From 59a27e2ce4ea270418813bdc0e218177616ab4b2 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sun, 4 Oct 2026 03:47:10 +0100 Subject: [PATCH 2/2] Settle the dark theme before inspecting TrueNAS help The first phone setup screenshot caught the light-to-dark transition, while the later polling screenshot was settled. Await the actual theme animations before capture so the browser receipt represents the displayed help rather than an intermediate colour frame. Contract-Neutral: Browser verification timing only; no product source or runtime contract change. Change-source: pulse-maintainer --- frontend-modern/browser-tests/truenas-connection-help.cjs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend-modern/browser-tests/truenas-connection-help.cjs b/frontend-modern/browser-tests/truenas-connection-help.cjs index 17a7fcfae9..3a6e38541a 100644 --- a/frontend-modern/browser-tests/truenas-connection-help.cjs +++ b/frontend-modern/browser-tests/truenas-connection-help.cjs @@ -35,7 +35,13 @@ const { chromium, webkit } = require('playwright'); { waitUntil: 'domcontentloaded', timeout: 60_000 }); const quickStart = page.getByRole('heading', { name: 'Quick Start', exact: true }); await quickStart.waitFor(); - if (engine === 'webkit') await page.evaluate(() => document.documentElement.classList.add('dark')); + if (engine === 'webkit') { + await page.evaluate(async () => { + document.documentElement.classList.add('dark'); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + await Promise.all(document.getAnimations().map((animation) => animation.finished.catch(() => {}))); + }); + } const article = await page.locator('article').innerText(); for (const text of ['it does not validate inventory or metric collection', 'an elapsed interval is not proof that a poll completed',