Document maintenance API and repair documentation section links

The API reference omitted the released resource maintenance endpoint, and
section links in the shipped viewer had no heading IDs to navigate to.
Document bounded windows, descendant scope and full-record replacement,
then make section links work after loading and reloading documentation.

Refs https://github.com/rcourtman/Pulse/discussions/1603
This commit is contained in:
rcourtman 2026-09-12 21:35:29 +01:00
parent c7c503994b
commit 5eeea72a3a
14 changed files with 354 additions and 26 deletions

View file

@ -41,6 +41,7 @@ Public endpoints include:
Some endpoints require admin privileges and/or scopes. Common scopes include:
- `monitoring:read`
- `monitoring:write`
- `settings:read`
- `settings:write`
- `agent:config:read`
@ -197,6 +198,66 @@ Report an incorrect merge (creates exclusions).
{ "sources": ["proxmox", "agent"], "notes": "optional note" }
```
### Resource Maintenance and Operator State
Use a timed maintenance window to suppress alerts during planned work on a
resource. This API, including descendant scope, is available in stable v6.4.1.
Choose the resource's `id` from `GET /api/resources` and URL-encode it in the
request path. Do not substitute a display name or a Proxmox VM number.
| Endpoint | Required scope | Result |
| --- | --- | --- |
| `GET /api/resources/{id}/operator-state` | `monitoring:read` | Returns the saved state, or HTTP 404 with `operator_state_not_set` when no state is saved. |
| `PUT /api/resources/{id}/operator-state` | `monitoring:write` | Replaces the entire state and returns the saved record. |
| `DELETE /api/resources/{id}/operator-state` | `monitoring:write` | Removes the entire state, returning HTTP 204 even if it was already absent. |
**PUT replaces the whole record, not just the fields supplied.** Read the current
state first and preserve unrelated settings, such as monitoring mode, lifecycle,
remediation policy, criticality and notes. Treat only `operator_state_not_set`
as an empty record, and stop on other read errors. Coordinate concurrent writers
so a read followed by a PUT does not overwrite someone else's changes.
For a one-time window, merge these fields into that record, replacing the example
times with your intended start and end:
```json
{
"maintenanceStartAt": "2026-10-15T20:00:00Z",
"maintenanceEndAt": "2026-10-15T20:30:00Z",
"maintenanceScope": "resource_and_descendants",
"maintenanceReason": "Proxmox node maintenance"
}
```
This is a set of fields to merge, not a complete replacement for an existing
record. Remove any `maintenanceRecurrence` when replacing a recurring schedule
with this one-time window. The API rejects a record containing both.
| Field | Meaning |
| --- | --- |
| `maintenanceStartAt`, `maintenanceEndAt` | RFC3339 timestamps. Supply both, with the end strictly after the start. The window is active from the start, inclusive, until the end, exclusive. |
| `maintenanceScope` | `resource` applies only to this resource and is the default. `resource_and_descendants` also covers descendants in Pulse's resource hierarchy, such as a node's guests. |
| `maintenanceReason` | Optional explanation of the planned work. |
The response includes `maintenanceWindowActive` and, while active,
`maintenanceActiveStartAt` and `maintenanceActiveEndAt`. The server sets the
record's `setAt` and `setBy` fields from the request, ignoring client values.
During an active window, matching new alerts and firing/recovery notifications
are suppressed. Applying an active window also clears matching existing alerts
from the active list, retaining history. This is broader than pausing delivery
for an existing incident. Resources outside the scope remain monitored normally.
The window expires automatically, with no re-enable request needed. Subsequent
observations can raise alerts again, subject to normal alert policy and any other
active maintenance window. Previously cleared alerts are not automatically
restored, and expiry does not replay a backlog of maintenance notifications.
To end maintenance early, read the latest record, remove its maintenance fields
and PUT the remaining state back. Do not DELETE the record unless you also intend
to remove its other operator settings. An inherited window must be changed on
the ancestor that owns it.
### Fleet Connections
`GET /api/connections`
Returns the canonical fleet connections ledger with per-row fleet-governance state. Requires admin access with `settings:read`.
@ -648,6 +709,10 @@ Common reporting error codes:
Alert configuration and history (requires `monitoring:read`/`monitoring:write`).
For planned downtime, use [Resource Maintenance and Operator State](#resource-maintenance-and-operator-state).
For an existing incident whose notifications should be paused without clearing
it, use the snooze and unsnooze endpoints below.
- `GET /api/alerts/config`
- `PUT /api/alerts/config`
- `GET /api/alerts/deadman/config` — returns only whether an external watchdog
@ -659,7 +724,7 @@ Alert configuration and history (requires `monitoring:read`/`monitoring:write`).
- `GET /api/alerts/deadman/status` — live watchdog health, monitor-loop
progress, sanitized delivery failure state, and the most recent restart
interruption; never includes the URL or endpoint fingerprint
- `POST /api/alerts/activate`
- `POST /api/alerts/activate` enables notification delivery and can notify about existing unacknowledged critical alerts. It is not a maintenance toggle, and there is no matching `/api/alerts/deactivate` endpoint.
- `GET /api/alerts/active`
- `GET /api/alerts/delivery-diagnosis?alertIdentifier=<alert-id>` (omit `alertIdentifier` to get the diagnosis array for every active alert)
- `GET /api/alerts/events?alertIdentifier=<alert-id>&type=<event-type,...>&since=<RFC3339>&limit=<n>` — append-only alert event log: lifecycle transitions and notification decisions, including suppressions with reasons; all parameters optional, newest first

View file

@ -20,6 +20,19 @@
## Purpose
### Resource maintenance API reference
The canonical and shipped API references document the existing authenticated
GET/PUT/DELETE `/api/resources/{id}/operator-state` contract. Read access requires
`monitoring:read`, and replacement or deletion requires `monitoring:write`.
Maintenance examples preserve unrelated operator settings and distinguish
resource-only scope from inherited descendant maintenance. Bounded expiry ends
maintenance suppression without a separate activation call. Applying active
maintenance clears matching active alerts while retaining history, rather than
promising incident-preserving snooze semantics. This documentation change adds
no route, permission or lifecycle behavior. The existing `docsLinks` checks keep
the shipped reference synchronized with `docs/API.md`.
### Ollama credential lifecycle
Settings return only `ollama_username` and `ollama_password_set`, never the
password. An omitted password preserves it, a supplied password replaces it

View file

@ -15,6 +15,14 @@
## Purpose
### Documentation heading dependency
The frontend locks `github-slugger` 2.0.0 for GitHub-compatible documentation
heading IDs. It adds no transitive dependency or installation script. The
dependency-security proof verifies its integrity-backed lock entry and bounded
installation footprint, while the shared documentation renderer tests cover
heading behavior. Existing DOMPurify sanitization remains the HTML boundary.
### Docker SDK dependency compatibility
The Docker consumers use Moby API v1.56.0 and client v0.6.0 together, without

View file

@ -20,6 +20,17 @@
## Purpose
### Shipped documentation fragment navigation
The shared documentation renderer assigns GitHub-compatible, document-local
heading IDs from sanitized text, retaining explicit anchors and avoiding
duplicate IDs. The documentation viewer follows fragments after asynchronous
content rendering as well as in-page navigation. Fragment targets receive
keyboard focus without entering the normal tab order. Missing or malformed
fragments do not throw or move focus. The renderer and fragment helper are
covered by `frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts`,
with direct-link, reload and keyboard navigation verified in the live viewer.
### Disk mount scrolling
DisksCard keeps every supplied mount in its parent's scrolling flow. It must not

View file

@ -5112,7 +5112,8 @@
"lane": "L8",
"contract": "docs/release-control/v6/internal/subsystems/frontend-primitives.md",
"owned_prefixes": [
"frontend-modern/src/components/shared/"
"frontend-modern/src/components/shared/",
"frontend-modern/src/features/docs/"
],
"owned_files": [
"frontend-modern/scripts/__tests__/planning-doc-status-audit.test.mjs",
@ -5213,6 +5214,7 @@
"frontend-modern/src/i18n/messages.ts",
"frontend-modern/src/i18n/policy.ts",
"frontend-modern/src/index.css",
"frontend-modern/src/pages/Docs.tsx",
"frontend-modern/src/routing/routePreload.ts",
"frontend-modern/src/stores/aiChat.ts",
"frontend-modern/src/stores/updates.ts",
@ -5881,6 +5883,22 @@
"frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts",
"frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts"
]
},
{
"id": "shipped-documentation-navigation",
"label": "sanitized documentation rendering and fragment navigation proof",
"match_prefixes": [
"frontend-modern/src/features/docs/"
],
"match_files": [
"frontend-modern/src/pages/Docs.tsx"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/features/docs/__tests__/docMarkdown.test.ts",
"frontend-modern/src/pages/__tests__/Docs.test.ts"
]
}
],
"match_files": null

View file

@ -1,37 +1,41 @@
{
"version": 1,
"base_sha": "72809bc1cf71e2493d1f0c492b926110a969bdb4",
"verified_at": "2026-09-11T10:26:20.316799Z",
"base_sha": "c7c503994b097023cc56f65434d35b46beac7918",
"verified_at": "2026-09-12T20:24:17Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/shared/cards/DisksCard.tsx"
"frontend-modern/src/features/docs/docMarkdown.ts",
"frontend-modern/src/pages/Docs.tsx"
],
"content_sha256": {
"frontend-modern/src/components/shared/cards/DisksCard.tsx": "cdda554f93b7aecb925d803b0192260ef1a445065c501468a0412b824dc08028"
"frontend-modern/src/features/docs/docMarkdown.ts": "b6408f3475721522eb22a9b8910e34b326b1c4cec045931baec05f4683c97c80",
"frontend-modern/src/pages/Docs.tsx": "c6c4b066c6af8a4eb8f01a9e03c2f939ee1592867dea2bf0f328bf111f99f2eb"
},
"routes": [
"/qualification/disks/?theme=light&count=2",
"/qualification/disks/?theme=dark&count=24"
"/docs/API",
"/docs/API#resource-maintenance-and-operator-state"
],
"viewports": [
{
"width": 600,
"height": 500
"width": 1440,
"height": 900
},
{
"width": 1200,
"height": 500
"width": 390,
"height": 844
}
],
"states": [
"2 and 24 mounts in light and dark themes",
"No nested clipping before scrolling; all mounts remain in outer flow"
"Final canonical and shipped API content agree",
"Maintenance endpoint table, example, expiry and preservation warnings readable",
"Fragment target visible and keyboard focused after delayed Markdown fetch",
"Malformed fragment leaves viewer usable"
],
"interactions": [
"Focus Before disks, keyboard End then Tab",
"Verify final mount in viewport and After disks focused",
"Inspect full-page Firefox dark narrow and Chromium light desktop screenshots"
],
"command": "pulse-heavy-run -- node tests/qualification/disk-mounts/check.mjs",
"notes": "16 synthetic production-card/CSS cases passed in Firefox142.0.1 and Chromium141.0.7390.37. Screenshots retained in lane outcome screenshots directory. No full installed drawer or reporter Firefox140 ESR reproduction claimed. Production runtime unchanged during completion repair."
"Hover and focus Alerts maintenance link, activate with Enter",
"Follow direct section URL and reload after delayed document fetch",
"Scroll to example and expiry instructions",
"Scroll narrow endpoint table horizontally and back",
"Inspect desktop and narrow screenshots with no page overflow"
]
}

View file

@ -11,6 +11,7 @@
"dependencies": {
"@solidjs/router": "^0.10.10",
"dompurify": "^3.4.13",
"github-slugger": "2.0.0",
"highlight.js": "^11.11.1",
"lucide-solid": "^0.545.0",
"marked": "^17.0.1",
@ -3979,6 +3980,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",

View file

@ -54,6 +54,7 @@
"dependencies": {
"@solidjs/router": "^0.10.10",
"dompurify": "^3.4.13",
"github-slugger": "2.0.0",
"highlight.js": "^11.11.1",
"lucide-solid": "^0.545.0",
"marked": "^17.0.1",

View file

@ -41,6 +41,7 @@ Public endpoints include:
Some endpoints require admin privileges and/or scopes. Common scopes include:
- `monitoring:read`
- `monitoring:write`
- `settings:read`
- `settings:write`
- `agent:config:read`
@ -197,6 +198,66 @@ Report an incorrect merge (creates exclusions).
{ "sources": ["proxmox", "agent"], "notes": "optional note" }
```
### Resource Maintenance and Operator State
Use a timed maintenance window to suppress alerts during planned work on a
resource. This API, including descendant scope, is available in stable v6.4.1.
Choose the resource's `id` from `GET /api/resources` and URL-encode it in the
request path. Do not substitute a display name or a Proxmox VM number.
| Endpoint | Required scope | Result |
| --- | --- | --- |
| `GET /api/resources/{id}/operator-state` | `monitoring:read` | Returns the saved state, or HTTP 404 with `operator_state_not_set` when no state is saved. |
| `PUT /api/resources/{id}/operator-state` | `monitoring:write` | Replaces the entire state and returns the saved record. |
| `DELETE /api/resources/{id}/operator-state` | `monitoring:write` | Removes the entire state, returning HTTP 204 even if it was already absent. |
**PUT replaces the whole record, not just the fields supplied.** Read the current
state first and preserve unrelated settings, such as monitoring mode, lifecycle,
remediation policy, criticality and notes. Treat only `operator_state_not_set`
as an empty record, and stop on other read errors. Coordinate concurrent writers
so a read followed by a PUT does not overwrite someone else's changes.
For a one-time window, merge these fields into that record, replacing the example
times with your intended start and end:
```json
{
"maintenanceStartAt": "2026-10-15T20:00:00Z",
"maintenanceEndAt": "2026-10-15T20:30:00Z",
"maintenanceScope": "resource_and_descendants",
"maintenanceReason": "Proxmox node maintenance"
}
```
This is a set of fields to merge, not a complete replacement for an existing
record. Remove any `maintenanceRecurrence` when replacing a recurring schedule
with this one-time window. The API rejects a record containing both.
| Field | Meaning |
| --- | --- |
| `maintenanceStartAt`, `maintenanceEndAt` | RFC3339 timestamps. Supply both, with the end strictly after the start. The window is active from the start, inclusive, until the end, exclusive. |
| `maintenanceScope` | `resource` applies only to this resource and is the default. `resource_and_descendants` also covers descendants in Pulse's resource hierarchy, such as a node's guests. |
| `maintenanceReason` | Optional explanation of the planned work. |
The response includes `maintenanceWindowActive` and, while active,
`maintenanceActiveStartAt` and `maintenanceActiveEndAt`. The server sets the
record's `setAt` and `setBy` fields from the request, ignoring client values.
During an active window, matching new alerts and firing/recovery notifications
are suppressed. Applying an active window also clears matching existing alerts
from the active list, retaining history. This is broader than pausing delivery
for an existing incident. Resources outside the scope remain monitored normally.
The window expires automatically, with no re-enable request needed. Subsequent
observations can raise alerts again, subject to normal alert policy and any other
active maintenance window. Previously cleared alerts are not automatically
restored, and expiry does not replay a backlog of maintenance notifications.
To end maintenance early, read the latest record, remove its maintenance fields
and PUT the remaining state back. Do not DELETE the record unless you also intend
to remove its other operator settings. An inherited window must be changed on
the ancestor that owns it.
### Fleet Connections
`GET /api/connections`
Returns the canonical fleet connections ledger with per-row fleet-governance state. Requires admin access with `settings:read`.
@ -648,6 +709,10 @@ Common reporting error codes:
Alert configuration and history (requires `monitoring:read`/`monitoring:write`).
For planned downtime, use [Resource Maintenance and Operator State](#resource-maintenance-and-operator-state).
For an existing incident whose notifications should be paused without clearing
it, use the snooze and unsnooze endpoints below.
- `GET /api/alerts/config`
- `PUT /api/alerts/config`
- `GET /api/alerts/deadman/config` — returns only whether an external watchdog
@ -659,7 +724,7 @@ Alert configuration and history (requires `monitoring:read`/`monitoring:write`).
- `GET /api/alerts/deadman/status` — live watchdog health, monitor-loop
progress, sanitized delivery failure state, and the most recent restart
interruption; never includes the URL or endpoint fingerprint
- `POST /api/alerts/activate`
- `POST /api/alerts/activate` enables notification delivery and can notify about existing unacknowledged critical alerts. It is not a maintenance toggle, and there is no matching `/api/alerts/deactivate` endpoint.
- `GET /api/alerts/active`
- `GET /api/alerts/delivery-diagnosis?alertIdentifier=<alert-id>` (omit `alertIdentifier` to get the diagnosis array for every active alert)
- `GET /api/alerts/events?alertIdentifier=<alert-id>&type=<event-type,...>&since=<RFC3339>&limit=<n>` — append-only alert event log: lifecycle transitions and notification decisions, including suppressions with reasons; all parameters optional, newest first

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import {
docAssetUrlForPath,
docRouteForPath,
@ -6,6 +6,7 @@ import {
normalizeDocPath,
renderDocMarkdown,
resolveDocLink,
scrollToDocFragment,
stripLeadingTitle,
wrapTables,
} from '../docMarkdown';
@ -86,6 +87,34 @@ describe('renderDocMarkdown', () => {
expect(html).toContain('<li>one</li>');
});
it('creates stable GitHub fragments from heading text, including duplicate headings', () => {
const source = '## Resource **Maintenance**\n\n## Resource Maintenance\n\n## Привет 你好';
const container = document.createElement('div');
container.innerHTML = renderDocMarkdown(source, 'API');
expect(Array.from(container.querySelectorAll('h2'), (heading) => heading.id)).toEqual([
'resource-maintenance',
'resource-maintenance-1',
'привет-你好',
]);
// Rendering another document must not inherit the first one's counters.
expect(renderDocMarkdown('## Resource Maintenance', 'OTHER')).toContain(
'id="resource-maintenance"',
);
});
it('keeps explicit anchors and avoids assigning their IDs to another heading', () => {
const container = document.createElement('div');
container.innerHTML = renderDocMarkdown(
'<a id="maintenance"></a>\n\n## Maintenance\n\n## Maintenance',
'API',
);
expect(container.querySelector('a')?.id).toBe('maintenance');
expect(Array.from(container.querySelectorAll('h2'), (heading) => heading.id)).toEqual([
'maintenance-1',
'maintenance-2',
]);
});
it('points intra-doc links at the viewer and marks them for routing', () => {
const html = renderDocMarkdown('[Install](INSTALL.md)', 'README');
expect(html).toContain('href="/docs/INSTALL"');
@ -131,6 +160,33 @@ describe('renderDocMarkdown', () => {
});
});
describe('scrollToDocFragment', () => {
it('scrolls and moves keyboard focus to an encoded fragment in the rendered document', () => {
const container = document.createElement('article');
container.innerHTML = renderDocMarkdown('## Привет 你好', 'API');
document.body.appendChild(container);
const heading = container.querySelector('h2')!;
heading.scrollIntoView = vi.fn();
try {
scrollToDocFragment(container, `#${encodeURIComponent(heading.id)}`);
expect(heading.scrollIntoView).toHaveBeenCalledWith({ block: 'start' });
expect(document.activeElement).toBe(heading);
expect(heading.tabIndex).toBe(-1);
} finally {
container.remove();
}
});
it('ignores absent or malformed fragments without moving focus', () => {
const container = document.createElement('article');
container.innerHTML = '<h2 id="maintenance">Maintenance</h2>';
const heading = container.querySelector('h2')!;
heading.scrollIntoView = vi.fn();
for (const hash of ['', '#', '#missing', '#%invalid']) scrollToDocFragment(container, hash);
expect(heading.scrollIntoView).not.toHaveBeenCalled();
});
});
describe('wrapTables', () => {
// Making the table itself display:block; overflow-x:auto is not enough, its
// rows still lay out past the block box and push the page into horizontal

View file

@ -1,5 +1,6 @@
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import GithubSlugger from 'github-slugger';
// Shipped documentation lives at /docs/<path>.md as a static asset. The
// rendered viewer is the same path without the .md suffix, which cannot
@ -148,6 +149,18 @@ export function rewriteDocLinks(html: string, currentDocPath: string): string {
if (typeof document === 'undefined') return html;
const container = document.createElement('div');
container.innerHTML = html;
// Repository links use GitHub heading fragments. Marked emits headings
// without IDs, so generate them from sanitized text and keep duplicate
// headings distinct without colliding with explicit document anchors.
const slugger = new GithubSlugger();
const ids = new Set(Array.from(container.querySelectorAll('[id]'), (element) => element.id));
container.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((heading) => {
if (heading.id) return;
let id = slugger.slug(heading.textContent ?? '');
while (ids.has(id)) id = slugger.slug(heading.textContent ?? '');
heading.id = id;
ids.add(id);
});
wrapTables(container);
container.querySelectorAll('a[href]').forEach((anchor) => {
@ -169,6 +182,24 @@ export function rewriteDocLinks(html: string, currentDocPath: string): string {
return container.innerHTML;
}
/** Follow a fragment after the asynchronously loaded document is rendered. */
export function scrollToDocFragment(container: HTMLElement, hash: string): void {
if (!hash || hash === '#') return;
let id: string;
try {
id = decodeURIComponent(hash.replace(/^#/, ''));
} catch {
return;
}
const target = Array.from(container.querySelectorAll<HTMLElement>('[id]')).find(
(element) => element.id === id,
);
if (!target) return;
target.tabIndex = -1;
target.scrollIntoView({ block: 'start' });
target.focus({ preventScroll: true });
}
/** First level-one heading, used as the document title. */
export function extractDocTitle(markdown: string, fallback: string): string {
const index = leadingTitleLineIndex(markdown);

View file

@ -1,5 +1,5 @@
import { useNavigate, useParams } from '@solidjs/router';
import { Show, createMemo, createResource } from 'solid-js';
import { useLocation, useNavigate, useParams } from '@solidjs/router';
import { Show, createEffect, createMemo, createResource, onCleanup } from 'solid-js';
import { PageHeader } from '@/components/shared/PageHeader';
import {
docAssetUrlForPath,
@ -7,6 +7,7 @@ import {
extractDocTitle,
normalizeDocPath,
renderDocMarkdown,
scrollToDocFragment,
stripLeadingTitle,
} from '@/features/docs/docMarkdown';
@ -37,6 +38,8 @@ export function isHtmlResponse(response: Pick<Response, 'headers'>): boolean {
export default function Docs() {
const params = useParams<{ docPath?: string }>();
const navigate = useNavigate();
const location = useLocation();
let article: HTMLElement | undefined;
const docPath = createMemo(() => normalizeDocPath(params.docPath ?? '') || DOCS_INDEX_PATH);
const [document] = createResource(docPath, fetchDoc);
@ -58,6 +61,19 @@ export default function Docs() {
return source ? renderDocMarkdown(stripLeadingTitle(source), docPath()) : '';
});
// A direct link or cross-document navigation can arrive before fetchDoc
// finishes. Retry once the rendered content exists, without timers or
// observers that could later pull the reader away from their position.
createEffect(() => {
const content = html();
const hash = location.hash;
if (!content || !hash) return;
const frame = requestAnimationFrame(() => {
if (article) scrollToDocFragment(article, hash);
});
onCleanup(() => cancelAnimationFrame(frame));
});
// Intra-documentation links are rewritten to viewer routes by the renderer.
// Delegate their clicks to the router so following one does not reload the
// application shell.
@ -112,13 +128,14 @@ export default function Docs() {
}
>
<article
ref={article}
// Typography renders inline code wrapped in literal backticks by
// default, which reads as unrendered markdown. Tables get their
// scroll container from wrapTables rather than prose utilities.
// Inline code must be able to break, because a long unbreakable
// URL in one otherwise pushes the whole page into horizontal
// scrolling on a phone; code inside pre keeps its own scrollbar.
class="prose prose-sm max-w-none dark:prose-invert prose-pre:overflow-x-auto prose-code:before:content-none prose-code:after:content-none [&_:not(pre)>code]:break-words"
class="prose prose-sm max-w-none dark:prose-invert prose-headings:scroll-mt-20 prose-pre:overflow-x-auto prose-code:before:content-none prose-code:after:content-none [&_:not(pre)>code]:break-words"
onClick={handleClick}
// eslint-disable-next-line solid/no-innerhtml -- renderDocMarkdown sanitises with DOMPurify
innerHTML={html()}

View file

@ -10,7 +10,15 @@ interface PackageManifest {
}
interface PackageLock {
packages: Record<string, { version?: string }>;
packages: Record<
string,
{
version?: string;
integrity?: string;
dependencies?: Record<string, string>;
hasInstallScript?: boolean;
}
>;
}
const manifest = JSON.parse(
@ -65,6 +73,15 @@ const dompurifyRangeIsPatched = (range: string): boolean =>
/^\^3\.\d+\.\d+$/.test(range) && atLeast(range.slice(1), [3, 4, 13]);
describe('frontend dependency security floors', () => {
it('locks the documentation heading helper without install scripts or transitive dependencies', () => {
const slugger = lock.packages['node_modules/github-slugger'];
expect(slugger).toBeDefined();
expect(manifest.dependencies['github-slugger']).toBe(slugger.version);
expect(slugger.integrity).toMatch(/^sha512-/);
expect(slugger.dependencies ?? {}).toEqual({});
expect(slugger.hasInstallScript).not.toBe(true);
});
it('keeps Vitest and its mocker above the redirect-mock file-read floor', () => {
// GHSA-82fw-gwwq-j7x9: the maintained 4.x fix starts at 4.1.11.
expect(manifest.devDependencies.vitest).toBe('^4.1.11');

View file

@ -131,6 +131,21 @@ describe('docsLinks', () => {
}
});
it('ships a bounded maintenance example separately from incident snoozing', () => {
const apiReference = readFileSync(path.join(repoRoot, 'docs', 'API.md'), 'utf8');
const maintenance = apiReference
.split('### Resource Maintenance and Operator State')[1]
.split('### Fleet Connections')[0];
const example = JSON.parse(/```json\s*([\s\S]*?)```/.exec(maintenance)![1]);
expect(Date.parse(example.maintenanceEndAt)).toBeGreaterThan(
Date.parse(example.maintenanceStartAt),
);
expect(example.maintenanceScope).toBe('resource_and_descendants');
expect(maintenance).toContain('PUT replaces the whole record');
expect(maintenance).toContain('Do not DELETE the record unless you also intend');
expect(apiReference).toContain('](#resource-maintenance-and-operator-state)');
});
it('ships the per-alert snooze and resume API contract', () => {
const apiReference = readFileSync(path.join(repoRoot, 'docs', 'API.md'), 'utf8');
const shippedAPIReference = readFileSync(