mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
Explain dated History readings before storage and licence changes
Make the existing guide distinguish same-time stored observations, missing metrics, independent panel times and qualified fallback. Link it from the guest-memory workflow, separate retained data from runtime access and explain the retired Relay compatibility limit without offering a new plan. Preserve read-only evidence and guest recovery boundaries. Contract-Neutral: Existing help, mirrors and regression/browser checks only; collectors, rendering behaviour, runtime grants, retention defaults and product routes are unchanged. Change-source: pulse-maintainer
This commit is contained in:
+69
-1
@@ -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,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; });
|
||||
@@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user