Integrate reviewed History interpretation help and browser proof

Preserve the exact reviewed c036461d and 594f5025 history. Retained source and production Docs desktop/narrow proof covers the unchanged guides; no monitoring, licence or release behaviour changes.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-12 20:24:25 +01:00
7 changed files with 563 additions and 2 deletions
+69 -1
View File
@@ -2,6 +2,72 @@
Pulse persists metrics history to disk so trend views and sparklines survive restarts.
## Read a History panel
Start with the existing chart, not a restart or a storage change. A current
reading does not prove that it was stored, and a selected time range does not
prove continuous coverage. **Window start** and **Window end** bound the
requested interval; they are not the first and last successful observations.
VM and LXC drawer panels can show these labels. Other charts and older Pulse
versions may differ; if a time or source is not shown, keep it unknown rather
than borrowing one from the current overview.
| Display | How to read it | What it does not establish |
| --- | --- | --- |
| **Latest stored** | The newest stored observation time in this panel. Each value belongs to that time; a dash means no stored observation for that metric at that time. | Not necessarily now, every metric's newest value, or another panel's time. |
| **Stored** while inspecting | Values at the selected stored observation time. | Not a live reading or an interpolated observation between points. |
| **current**, **last known** or **freshness unknown** with no stored history | A separately qualified fallback reading. Keep its label and explanation. | Not proof that the value was persisted or belongs to a past incident. |
| **Single observation. No trend yet.** | One stored time in that panel. | Not a flat trend or sustained recovery. |
| **Missing observations** | Some series lack readings at other stored times in that panel; lines stop across those gaps. | Not the cause or duration of a monitoring outage. Missing is not measured zero. |
For example, CPU **22% at 12:02** and memory **95% at 12:01** are not one
observation. At **Latest stored · 12:02**, memory is a dash if there is no
memory point at 12:02; the earlier 95% remains inspectable. Network and disk
I/O panels have their own times, not a shared snapshot of the guest.
1. Keep the same resource, metric and range when comparing existing readings.
**Inspect history**, when offered, steps through stored times: focus the
slider and use the arrow keys, **Home** or **End**, or move it by touch.
Hover is another way to inspect; it is not required. Leaving the slider
returns to the latest stored display.
2. Keep the displayed observation time and qualification with the value.
Stored points can be aggregated or downsampled; they are not a measurement
of every instant in the selected range. A page refresh time is not the
observation time. If an absolute time is unavailable, retain that limit.
3. Compare an alert or Patrol finding with the original episode, not just
today's overview. Today's source cannot identify the source of a past
warning. Follow [guest memory interpretation](TROUBLESHOOTING.md#guest-memory-is-high-but-available-memory-is-plentiful)
when unlike readings suggest memory pressure.
Use already available observations; do not run Diagnostics or guest-agent
probes, restart a guest, repeat a backup, clear History or change retention to
make a comparison agree. An unresponsive guest or an uncertain freeze/thaw
operation needs [Backup safety](VM_DISK_MONITORING.md#backup-safety), not chart
inspection. When reporting a display disagreement, keep the panel, time,
range, labels and relevant values with consistent private aliases; do not
share raw responses, credentials or a full network export.
## Retention and accessible range
Storage retention and the range your running instance can serve are separate.
Community supports up to **7 days**; Pro and legacy Pro+ entitlements support
up to **90 days**. The panel uses the active runtime's history limit, not the
length of time the database has existed. A longer retention setting does not
grant access, and a licence cannot recover observations that were never stored.
**Relay is no longer sold.** Continuing Relay subscribers receive Pro features,
including 90-day history, at their existing price. Older Relay payloads can
still show a 14-day compatibility limit; that is not a plan to buy or proof
that a continuing subscription has received its current entitlement. See
[plans and legacy subscriptions](PULSE_PRO.md#self-hosted-commercial-model).
An API request beyond the active limit returns **402 Payment Required**, not
an empty history or a failed collector. Use an allowed range and the existing
licence/range display first; do not buy another licence, reset history or
reinstall merely to diagnose it. Storage defaults below do not guarantee that
every resource has data for every allowed range.
## Storage Location
Metrics history is stored in a SQLite database named `metrics.db` under the Pulse data directory:
@@ -178,7 +244,9 @@ durable historical coverage. An HTTP 200 with no points proves neither zero
usage nor successful collection, and a non-zero current value does not prove
every historical interval is present.
> **License**: Requests beyond Community's `7d` floor require the paid `long_term_metrics` entitlement. Relay unlocks `14d`, Pro and legacy Pro+ unlock `90d`, and requests beyond the active tier's limit return `402 Payment Required`.
> **History limit**: Requests beyond the active runtime's allowed range return
> `402 Payment Required`. See [retention and accessible range](#retention-and-accessible-range);
> stored retention is not access or proof of collected observations.
> **Older examples**: `container`, `dockerHost`, `dockerContainer`, `guest` and
> `docker` are not current `resourceType` query values. Use the chart's current
> type rather than a display label or internal storage name.
+5
View File
@@ -680,6 +680,11 @@ same units and memory allocation. The Proxmox node's available memory is not the
VM's available memory, and a single application's RSS is not total guest usage.
Do not sum process RSS to infer guest pressure: processes can share pages.
For History comparisons, keep each panel's stored time and qualification with
the value; a range endpoint, page refresh or today's live source does not date
a past reading. Use [History panel interpretation](METRICS_HISTORY.md#read-a-history-panel)
to distinguish a missing observation from zero, and stored data from fallback.
| Reading | What it describes | What it does not establish |
| --- | --- | --- |
| Linux `available` / `MemAvailable` | An estimate of memory usable for new allocations without swapping, including some reclaimable cache. | It is not just `free`, a guarantee of every allocation succeeding, or a reading for another guest. |
@@ -0,0 +1,164 @@
// Exercise only existing help: real Docs fetch, renderer, routes and CSS.
// No monitored system, diagnostics, licence change or History collector runs.
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { chromium, webkit } = require('playwright');
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest('hex');
(async () => {
const root = '/workspace/frontend-modern';
const artifacts = path.join(root, 'node_modules', 'metrics-history-help-browser');
const sourceDocs = Object.fromEntries(
['METRICS_HISTORY', 'TROUBLESHOOTING', 'PULSE_PRO', 'VM_DISK_MONITORING'].map((name) => {
const bytes = fs.readFileSync(path.join(root, `public/docs/${name}.md`));
assert.deepEqual(bytes, fs.readFileSync(`/workspace/docs/${name}.md`));
return [name, { bytes: bytes.length, sha256: sha256(bytes) }];
}),
);
process.chdir(root);
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-metrics-history-help'),
server: { host: '127.0.0.1', port: 5257, strictPort: true },
optimizeDeps: { noDiscovery: true, include: [] },
});
const results = [];
let browser;
try {
await server.listen();
for (const [engine, width, dark] of [['chromium', 1280, false], ['webkit', 390, true]]) {
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 = [], backendRequests = [], mutations = [], requests = [], captures = [];
await page.addInitScript(() => {
const fetch = window.fetch.bind(window);
window.__historyHelpDocuments = [];
window.fetch = async (...args) => {
const response = await fetch(...args);
const pathname = new URL(response.url).pathname;
if (/^\/docs\/.+\.md$/.test(pathname)) {
const bytes = Array.from(new Uint8Array(await response.clone().arrayBuffer()));
window.__historyHelpDocuments.push({ pathname, status: response.status, bytes });
}
return response;
};
});
page.on('pageerror', (error) => errors.push(error.message));
page.on('console', (message) => {
if (message.type() === 'error') errors.push(message.text());
});
page.on('request', (request) => {
requests.push(new URL(request.url()).pathname);
if (!['GET', 'HEAD'].includes(request.method())) mutations.push(request.method());
});
await page.route('**/api/**', async (route) => {
backendRequests.push(new URL(route.request().url()).pathname);
await route.abort();
});
const navigate = async (route, fragment) => {
await page.evaluate((href) => {
history.pushState({}, '', href);
dispatchEvent(new PopStateEvent('popstate'));
}, route);
await focused(fragment);
};
const focused = async (id) => {
await page.waitForFunction((fragment) => {
const target = document.getElementById(fragment);
const rect = target?.getBoundingClientRect();
return target && document.activeElement === target && rect.top >= 0 && rect.top < innerHeight;
}, id);
};
const activate = async (name, id) => {
const link = page.getByRole('link', { name, exact: true });
await link.focus();
await page.keyboard.press('Enter');
await focused(id);
};
const capture = async (name) => {
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1));
await page.waitForFunction(() => document.getAnimations().length === 0);
const filename = `${engine}-${name}.png`;
const filepath = path.join(artifacts, filename);
await page.screenshot({ path: filepath });
captures.push({ filename, bytes: fs.statSync(filepath).size, sha256: sha256(fs.readFileSync(filepath)) });
};
await page.goto('http://127.0.0.1:5257/browser-tests/docs-fragment-navigation.html', {
waitUntil: 'domcontentloaded', timeout: 60000,
});
if (dark) await page.evaluate(() => document.documentElement.classList.add('dark'));
await navigate('/docs/TROUBLESHOOTING#guest-memory-is-high-but-available-memory-is-plentiful',
'guest-memory-is-high-but-available-memory-is-plentiful');
assert.ok((await page.locator('article').innerText()).includes("today's live source does not date a past reading"));
await capture('entry');
await activate('History panel interpretation', 'read-a-history-panel');
const section = await page.evaluate(() => {
const nodes = [];
for (let node = document.getElementById('read-a-history-panel').nextElementSibling;
node && node.tagName !== 'H2'; node = node.nextElementSibling) nodes.push(node.textContent);
return nodes.join(' ').replace(/\s+/g, ' ');
});
for (const phrase of [
'Latest stored', 'Single observation. No trend yet.', 'Missing is not measured zero',
'CPU 22% at 12:02 and memory 95% at 12:01 are not one observation',
'arrow keys, Home or End, or move it by touch',
"Today's source cannot identify the source of a past warning",
'do not run Diagnostics or guest-agent probes', 'do not share raw responses, credentials',
]) assert.ok(section.includes(phrase), `${engine}: ${phrase}`);
await capture('observations');
await page.getByText('For example, CPU', { exact: false }).scrollIntoViewIfNeeded();
await capture('comparison');
await navigate('/docs/METRICS_HISTORY#retention-and-accessible-range', 'retention-and-accessible-range');
const rendered = await page.locator('article').innerText();
for (const phrase of [
'Community supports up to 7 days', 'Relay is no longer sold',
'14-day compatibility limit', 'not a plan to buy', '402 Payment Required',
'do not buy another licence, reset history or reinstall',
]) assert.ok(rendered.replace(/\s+/g, ' ').includes(phrase), `${engine}: ${phrase}`);
assert.ok(!rendered.includes('Relay unlocks'));
await capture('range');
await activate('plans and legacy subscriptions', 'self-hosted-commercial-model');
assert.ok((await page.locator('article').innerText()).includes('not the entitlement for continuing Relay subscribers'));
await navigate('/docs/METRICS_HISTORY#read-a-history-panel', 'read-a-history-panel');
await activate('Backup safety', 'backup-safety');
assert.ok((await page.locator('article').innerText()).includes('every filesystem covered by the backup'));
const documents = await page.evaluate(() => window.__historyHelpDocuments);
const consumed = documents.filter((doc) => sourceDocs[doc.pathname.split('/').at(-1).replace('.md', '')])
.map((doc) => ({ pathname: doc.pathname, status: doc.status,
bytes: doc.bytes.length, sha256: sha256(Buffer.from(doc.bytes)) }));
for (const [name, identity] of Object.entries(sourceDocs)) {
const responses = consumed.filter((doc) => doc.pathname === `/docs/${name}.md`);
assert.ok(responses.length >= 1, `${engine}: missing ${name} consumer`);
for (const response of responses) {
assert.equal(response.status, 200);
assert.equal(response.bytes, identity.bytes);
assert.equal(response.sha256, identity.sha256);
}
}
assert.deepEqual(backendRequests, []);
assert.deepEqual(mutations, []);
assert.deepEqual(errors, []);
results.push({ engine, browserVersion: browser.version(), width, dark, consumed, captures,
memoryEntryKeyboardNavigation: true, plansKeyboardNavigation: true, safetyKeyboardNavigation: true,
directRangeFragmentFocus: true, noHorizontalPageOverflow: true, backendRequests, mutations, errors,
requestedSourcePaths: [...new Set(requests.filter((url) => url.startsWith('/src/')))].sort() });
await browser.close();
browser = undefined;
}
} finally {
if (browser) await browser.close();
await server.close();
}
const result = { result: 'passed', playwright: require('playwright/package.json').version,
sourceDocs, results, browserAndServerClosed: true,
scope: 'Production Docs consumer and exact shipped help bytes; no native guest, collector, licence migration, full application or release acceptance.' };
fs.writeFileSync(path.join(artifacts, 'result.json'), JSON.stringify(result, null, 2) + '\n');
console.log(JSON.stringify(result));
})().catch((error) => { console.error(error); process.exitCode = 1; });
@@ -0,0 +1,131 @@
{
"version": 1,
"result": "passed",
"base_sha": "b54a2fa3f70d7df05e5d3fe1d3733326c2332da8",
"verified_at": "2026-10-12T18:23:23.779839Z",
"changed_paths": [
"docs/METRICS_HISTORY.md",
"docs/TROUBLESHOOTING.md",
"frontend-modern/public/docs/METRICS_HISTORY.md",
"frontend-modern/public/docs/TROUBLESHOOTING.md"
],
"content_sha256": {
"docs/METRICS_HISTORY.md": "2f3e78e9d066f2b7e221b6ed351c4cd8f7b40567a6d7b5e47332376ade0c6b11",
"docs/TROUBLESHOOTING.md": "cf128b8d4a3edb4595e57536d3370de0be636dd3300736efa4a6501c53861967",
"frontend-modern/public/docs/METRICS_HISTORY.md": "2f3e78e9d066f2b7e221b6ed351c4cd8f7b40567a6d7b5e47332376ade0c6b11",
"frontend-modern/public/docs/TROUBLESHOOTING.md": "cf128b8d4a3edb4595e57536d3370de0be636dd3300736efa4a6501c53861967"
},
"routes": [
"/docs/TROUBLESHOOTING#guest-memory-is-high-but-available-memory-is-plentiful",
"/docs/METRICS_HISTORY#read-a-history-panel",
"/docs/METRICS_HISTORY#retention-and-accessible-range",
"/docs/PULSE_PRO#self-hosted-commercial-model",
"/docs/VM_DISK_MONITORING#backup-safety"
],
"states": [
"Stored-time interpretation table: latest stored, selected stored, qualified fallback, single observation and missing observations",
"Unequal CPU/memory observation times and keyboard/touch History guidance",
"Stored retention versus active accessible range, retired Relay sale and continuing Pro subscriber terms",
"Guest-memory entry and linked backup-safety help"
],
"interactions": [
"Keyboard activation of the existing guest-memory History interpretation link, with fragment focus and scroll",
"Direct range fragment navigation, plans link and backup-safety link with target focus",
"Scroll the comparison example at the narrow touch viewport",
"Inspect eight retained capture files natively; all table columns and new links remain readable without horizontal page overflow"
],
"viewports": [
{
"width": 1280,
"height": 900,
"browser": "Chromium 141.0.7390.37",
"theme": "light"
},
{
"width": 390,
"height": 900,
"browser": "WebKit 26.0",
"theme": "dark",
"touch": true
}
],
"browser": {
"client": "Playwright 1.56.1",
"driver": "frontend-modern/browser-tests/metrics-history-help.cjs"
},
"source_help_commit": "c036461de74c0e71932e7e481dc35ead26bbe685",
"containing_source_sha256": {
"docs/METRICS_HISTORY.md": "2f3e78e9d066f2b7e221b6ed351c4cd8f7b40567a6d7b5e47332376ade0c6b11",
"docs/PULSE_PRO.md": "776235ec660c1c53fef781e99b024bd2030d218afb8a601929e4bb5cf6b2dffb",
"docs/TROUBLESHOOTING.md": "cf128b8d4a3edb4595e57536d3370de0be636dd3300736efa4a6501c53861967",
"docs/VM_DISK_MONITORING.md": "88da3f28184e78b724f2bf59e39cbb1b7be65f8094ca2a3197c3d88875155b8e",
"frontend-modern/browser-tests/docs-fragment-navigation.html": "8303e101060aabd3247446676624d0f83b2cf81b9f6b1b71f180ea3432f66208",
"frontend-modern/browser-tests/docs-fragment-navigation.tsx": "2de6b8c853e5cd87790dcb6e582fefae07661d449835366cb72f2623b1bfe053",
"frontend-modern/browser-tests/metrics-history-help.cjs": "45b4189d641b6ec75fdb0dfd9889ef0d6ba6ca14edeac2f55e263cb4f7d2e674",
"frontend-modern/package-lock.json": "230d8a9ef15f68577ba07691753a52b4baad05b36b9d6058993955d3fe480ecb",
"frontend-modern/package.json": "e00d2edb9f70b43c7e43ef3d79195fc2148b58772c00a3e130fe721f0eec76ee",
"frontend-modern/public/docs/METRICS_HISTORY.md": "2f3e78e9d066f2b7e221b6ed351c4cd8f7b40567a6d7b5e47332376ade0c6b11",
"frontend-modern/public/docs/PULSE_PRO.md": "776235ec660c1c53fef781e99b024bd2030d218afb8a601929e4bb5cf6b2dffb",
"frontend-modern/public/docs/TROUBLESHOOTING.md": "cf128b8d4a3edb4595e57536d3370de0be636dd3300736efa4a6501c53861967",
"frontend-modern/public/docs/VM_DISK_MONITORING.md": "88da3f28184e78b724f2bf59e39cbb1b7be65f8094ca2a3197c3d88875155b8e",
"frontend-modern/src/components/shared/PageHeader.tsx": "428d4c02380ba13a38d1ee7404c56b538562df904aecb66f11786ef0dbb8d259",
"frontend-modern/src/features/docs/docMarkdown.ts": "572a3b4bc5bc1114adef5aa64bb0ea833c0dbe5865bbd57d3dc767fd1965aa55",
"frontend-modern/src/hooks/useKioskMode.ts": "31a30251cb828b2a32977143707f5cc1b57d980aa2228d6e09b26b000162cc0b",
"frontend-modern/src/index.css": "df9316666c4dab61acd0528944690c3f8f08f890fd49bfa9efcbfea68524664c",
"frontend-modern/src/pages/Docs.tsx": "2ddb5a1daef50d23a99a7d1cb510c73cd7a85600f07ea9f69ad72f741f3b5229",
"frontend-modern/src/utils/url.ts": "94c3a37354a53b38169ed41188f1c4d18d2d04c829b6c8f9573d2fb2d28a93dc",
"frontend-modern/vite.config.ts": "de649c3466c8369b7ceff630b72f3733d1123417d6ef1964903450d64d796fb2",
"tests/integration/package-lock.json": "d6241c8c3ad5ccd1c97c749ea70e0b707a2dc5eabd8601faf19c78b8f79b975a"
},
"captures": [
{
"filename": "chromium-entry.png",
"bytes": 212900,
"sha256": "c40718b94d55645aa8389c815d793f9b673e3c7f3928af55ab5341873ea04d87"
},
{
"filename": "chromium-observations.png",
"bytes": 177877,
"sha256": "f993e99b0d895a8999642430718ee358220cfcc9dc52ea70650e5d996307c572"
},
{
"filename": "chromium-comparison.png",
"bytes": 177877,
"sha256": "f993e99b0d895a8999642430718ee358220cfcc9dc52ea70650e5d996307c572"
},
{
"filename": "chromium-range.png",
"bytes": 146986,
"sha256": "8a3102b040206bb83062c4a81befc7e3deb60118923551998a11510751ba09a0"
},
{
"filename": "webkit-entry.png",
"bytes": 129297,
"sha256": "120dce35f9200582d2a56957981b96dd239810bdc230308ac9e5da00a4b0c435"
},
{
"filename": "webkit-observations.png",
"bytes": 110289,
"sha256": "92ddf9b74d610be0f8bbd47de520b73af413c6a8ca3630441580bef5e00778dd"
},
{
"filename": "webkit-comparison.png",
"bytes": 125240,
"sha256": "fad1d40830e85f736ae5a983719cf38ec52f7635bbe41e2795cee024916a884d"
},
{
"filename": "webkit-range.png",
"bytes": 137572,
"sha256": "7b164d7ccdd59544af4f4df4d99ff9f4686ba1e200ae6f30c4f9e5add8b4eac5"
}
],
"pixel_observations": [
"Desktop: focus outlines identify the guest-memory, History and range headings; the three-column table, 12:02/12:01 example, plan qualification and underlined links are legible and unclipped. The two desktop History captures are byte-identical because the comparison example was already visible.",
"Narrow dark WebKit: table columns wrap within 390 pixels; focus heading, missing-not-zero qualification, unequal-time example, History link and retired-Relay/range text remain legible. Vertical scroll is needed for the longer table and page, not horizontal scrolling."
],
"backend_requests": [],
"mutations": [],
"console_and_page_errors": [],
"browser_and_server_closed": true,
"scope": "Existing production Docs/router/Markdown/CSS consumer and exact help bytes. No native guest, monitoring collector, licence migration, full authenticated app, release or installed-recovery acceptance."
}
+69 -1
View File
@@ -2,6 +2,72 @@
Pulse persists metrics history to disk so trend views and sparklines survive restarts.
## Read a History panel
Start with the existing chart, not a restart or a storage change. A current
reading does not prove that it was stored, and a selected time range does not
prove continuous coverage. **Window start** and **Window end** bound the
requested interval; they are not the first and last successful observations.
VM and LXC drawer panels can show these labels. Other charts and older Pulse
versions may differ; if a time or source is not shown, keep it unknown rather
than borrowing one from the current overview.
| Display | How to read it | What it does not establish |
| --- | --- | --- |
| **Latest stored** | The newest stored observation time in this panel. Each value belongs to that time; a dash means no stored observation for that metric at that time. | Not necessarily now, every metric's newest value, or another panel's time. |
| **Stored** while inspecting | Values at the selected stored observation time. | Not a live reading or an interpolated observation between points. |
| **current**, **last known** or **freshness unknown** with no stored history | A separately qualified fallback reading. Keep its label and explanation. | Not proof that the value was persisted or belongs to a past incident. |
| **Single observation. No trend yet.** | One stored time in that panel. | Not a flat trend or sustained recovery. |
| **Missing observations** | Some series lack readings at other stored times in that panel; lines stop across those gaps. | Not the cause or duration of a monitoring outage. Missing is not measured zero. |
For example, CPU **22% at 12:02** and memory **95% at 12:01** are not one
observation. At **Latest stored · 12:02**, memory is a dash if there is no
memory point at 12:02; the earlier 95% remains inspectable. Network and disk
I/O panels have their own times, not a shared snapshot of the guest.
1. Keep the same resource, metric and range when comparing existing readings.
**Inspect history**, when offered, steps through stored times: focus the
slider and use the arrow keys, **Home** or **End**, or move it by touch.
Hover is another way to inspect; it is not required. Leaving the slider
returns to the latest stored display.
2. Keep the displayed observation time and qualification with the value.
Stored points can be aggregated or downsampled; they are not a measurement
of every instant in the selected range. A page refresh time is not the
observation time. If an absolute time is unavailable, retain that limit.
3. Compare an alert or Patrol finding with the original episode, not just
today's overview. Today's source cannot identify the source of a past
warning. Follow [guest memory interpretation](TROUBLESHOOTING.md#guest-memory-is-high-but-available-memory-is-plentiful)
when unlike readings suggest memory pressure.
Use already available observations; do not run Diagnostics or guest-agent
probes, restart a guest, repeat a backup, clear History or change retention to
make a comparison agree. An unresponsive guest or an uncertain freeze/thaw
operation needs [Backup safety](VM_DISK_MONITORING.md#backup-safety), not chart
inspection. When reporting a display disagreement, keep the panel, time,
range, labels and relevant values with consistent private aliases; do not
share raw responses, credentials or a full network export.
## Retention and accessible range
Storage retention and the range your running instance can serve are separate.
Community supports up to **7 days**; Pro and legacy Pro+ entitlements support
up to **90 days**. The panel uses the active runtime's history limit, not the
length of time the database has existed. A longer retention setting does not
grant access, and a licence cannot recover observations that were never stored.
**Relay is no longer sold.** Continuing Relay subscribers receive Pro features,
including 90-day history, at their existing price. Older Relay payloads can
still show a 14-day compatibility limit; that is not a plan to buy or proof
that a continuing subscription has received its current entitlement. See
[plans and legacy subscriptions](PULSE_PRO.md#self-hosted-commercial-model).
An API request beyond the active limit returns **402 Payment Required**, not
an empty history or a failed collector. Use an allowed range and the existing
licence/range display first; do not buy another licence, reset history or
reinstall merely to diagnose it. Storage defaults below do not guarantee that
every resource has data for every allowed range.
## Storage Location
Metrics history is stored in a SQLite database named `metrics.db` under the Pulse data directory:
@@ -178,7 +244,9 @@ durable historical coverage. An HTTP 200 with no points proves neither zero
usage nor successful collection, and a non-zero current value does not prove
every historical interval is present.
> **License**: Requests beyond Community's `7d` floor require the paid `long_term_metrics` entitlement. Relay unlocks `14d`, Pro and legacy Pro+ unlock `90d`, and requests beyond the active tier's limit return `402 Payment Required`.
> **History limit**: Requests beyond the active runtime's allowed range return
> `402 Payment Required`. See [retention and accessible range](#retention-and-accessible-range);
> stored retention is not access or proof of collected observations.
> **Older examples**: `container`, `dockerHost`, `dockerContainer`, `guest` and
> `docker` are not current `resourceType` query values. Use the chart's current
> type rather than a display label or internal storage name.
@@ -680,6 +680,11 @@ same units and memory allocation. The Proxmox node's available memory is not the
VM's available memory, and a single application's RSS is not total guest usage.
Do not sum process RSS to infer guest pressure: processes can share pages.
For History comparisons, keep each panel's stored time and qualification with
the value; a range endpoint, page refresh or today's live source does not date
a past reading. Use [History panel interpretation](METRICS_HISTORY.md#read-a-history-panel)
to distinguish a missing observation from zero, and stored data from fallback.
| Reading | What it describes | What it does not establish |
| --- | --- | --- |
| Linux `available` / `MemAvailable` | An estimate of memory usable for new allocations without swapping, including some reclaimable cache. | It is not just `free`, a guarantee of every allocation succeeding, or a reading for another guest. |
@@ -0,0 +1,120 @@
import { readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { renderDocMarkdown } from '@/features/docs/docMarkdown';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
const read = (file: string) => readFileSync(path.join(root, file), 'utf8');
const guide = read('docs/METRICS_HISTORY.md');
const render = (markdown: string, name: string) => {
const article = document.createElement('article');
article.innerHTML = renderDocMarkdown(markdown, name);
return article;
};
const article = () => render(guide, 'METRICS_HISTORY');
const interpretation = () =>
render(guide.split('## Read a History panel')[1].split('\n## ')[0], 'METRICS_HISTORY');
const text = (element: Element) => element.textContent?.replace(/\s+/g, ' ');
describe('History interpretation help', () => {
it('ships both changed guides and puts safe interpretation before storage tuning', () => {
for (const name of ['METRICS_HISTORY', 'TROUBLESHOOTING']) {
expect(read(`frontend-modern/public/docs/${name}.md`)).toBe(read(`docs/${name}.md`));
}
const headings = [...article().querySelectorAll('h2')].map((heading) => heading.id);
expect(headings.slice(0, 3)).toEqual([
'read-a-history-panel',
'retention-and-accessible-range',
'storage-location',
]);
expect(interpretation().querySelector('pre')).toBeNull();
});
it('explains real drawer labels without treating older or fallback readings as current', () => {
const rows = [...interpretation().querySelectorAll('tbody tr')].map((row) => text(row));
expect(rows).toHaveLength(5);
expect(rows[0]).toContain("Not necessarily now, every metric's newest value");
expect(rows[1]).toContain('Not a live reading or an interpolated observation');
expect(rows[2]).toContain('Not proof that the value was persisted');
expect(rows[3]).toContain('Not a flat trend or sustained recovery');
expect(rows[4]).toContain('Not the cause or duration of a monitoring outage');
const renderer = read('frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx');
for (const label of [
'Latest stored',
'Stored',
'Inspect history',
'Single observation. No trend yet.',
]) {
expect(renderer).toContain(label);
expect(text(interpretation())).toContain(label);
}
expect(text(interpretation())).toContain('Other charts and older Pulse versions may differ');
expect(text(interpretation())).toContain('keep it unknown rather than borrowing one');
});
it('keeps same-time values, missing observations, panel-local times and access to earlier points distinct', () => {
expect(text(interpretation())).toContain(
'CPU 22% at 12:02 and memory 95% at 12:01 are not one observation',
);
expect(text(interpretation())).toContain('the earlier 95% remains inspectable');
expect(text(interpretation())).toContain('Network and disk I/O panels have their own times');
expect(text(interpretation())).toContain('Missing is not measured zero');
const steps = [...interpretation().querySelectorAll('ol > li')].map((step) => text(step));
expect(steps).toHaveLength(3);
expect(steps[0]).toContain('arrow keys, Home or End, or move it by touch');
expect(steps[1]).toContain('aggregated or downsampled');
expect(steps[2]).toContain("Today's source cannot identify the source of a past warning");
});
it('links from the existing memory workflow to working interpretation and safety headings', () => {
const troubleshooting = render(read('docs/TROUBLESHOOTING.md'), 'TROUBLESHOOTING');
const href = '/docs/METRICS_HISTORY#read-a-history-panel';
expect(troubleshooting.querySelector(`a[href="${href}"]`)?.textContent).toBe(
'History panel interpretation',
);
expect(article().querySelector('#read-a-history-panel')).not.toBeNull();
const safety = '/docs/VM_DISK_MONITORING#backup-safety';
expect(interpretation().querySelector(`a[href="${safety}"]`)?.textContent).toBe(
'Backup safety',
);
expect(
render(read('docs/VM_DISK_MONITORING.md'), 'VM_DISK_MONITORING').querySelector(
'#backup-safety',
),
).not.toBeNull();
expect(text(interpretation())).toContain('do not run Diagnostics or guest-agent probes');
expect(text(interpretation())).toContain('clear History or change retention');
expect(text(interpretation())).toContain(
'do not share raw responses, credentials or a full network export',
);
});
it('separates storage from runtime access and does not sell the retired Relay compatibility tier', () => {
const section = guide.split('## Retention and accessible range')[1].split('\n## ')[0];
const rendered = render(section, 'METRICS_HISTORY');
expect(text(rendered)).toContain('Community supports up to 7 days');
expect(text(rendered)).toContain('Pro and legacy Pro+ entitlements support up to 90 days');
expect(text(rendered)).toContain('Relay is no longer sold');
expect(text(rendered)).toContain('Continuing Relay subscribers receive Pro features');
expect(text(rendered)).toContain('14-day compatibility limit');
expect(text(rendered)).toContain('not a plan to buy');
expect(text(rendered)).toContain('402 Payment Required, not an empty history');
expect(text(rendered)).toContain('do not buy another licence, reset history or reinstall');
expect(text(rendered)).toContain('cannot recover observations that were never stored');
expect(guide).not.toContain('Relay unlocks');
const link = rendered.querySelector('a[href="/docs/PULSE_PRO#self-hosted-commercial-model"]');
expect(link?.textContent).toBe('plans and legacy subscriptions');
expect(
render(read('docs/PULSE_PRO.md'), 'PULSE_PRO').querySelector('#self-hosted-commercial-model'),
).not.toBeNull();
const limits = read('pkg/licensing/features.go');
expect(limits).toMatch(/TierFree:\s+7,/);
expect(limits).toMatch(/TierRelay:\s+14,/);
expect(limits).toMatch(/TierPro:\s+90,/);
expect(limits).toMatch(/TierProPlus:\s+90,/);
expect(read('frontend-modern/src/stores/license.ts')).toContain(
'runtimeCapabilities()?.max_history_days',
);
});
});