Keep endpoint TLS test feedback beside the member being tested

Show scoped results locally and announce/reveal them without stealing focus; primary results stay in the existing footer. Invalidate them with trust edits. Contract and consumer checks cover the existing settings flow.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-27 09:01:16 +00:00
parent dfbe3fa91c
commit 30714c1a56
10 changed files with 134 additions and 2 deletions
@@ -15,6 +15,12 @@
## Purpose
A member test announces and reveals its result beside that exact member rather
than leaving feedback in the off-screen global footer. The normal primary
Test connection result stays in the footer; both remain endpoint-qualified
and do not update background or agent health. The member-section test and
browser interaction proof check the locally scoped live result.
The connection editor exposes each member's actual runtime TLS endpoint.
Its pin/CA controls submit only deliberately edited scopes, distinguish unknown
first-use trust, and leave primary and other-member policy independent. Tests
@@ -21,6 +21,12 @@
## Purpose
A member test announces and reveals its result beside that exact member rather
than leaving feedback in the off-screen global footer. The normal primary
Test connection result stays in the footer; both remain endpoint-qualified
and do not update background or agent health. The member-section test and
browser interaction proof check the locally scoped live result.
The node API client retains server-projected TLS endpoint and first-use fields,
serialises explicit per-endpoint empty pin choices, and keeps credentials out
of saved-connection test payloads. Nonempty malformed pin strings are preserved,
@@ -15,6 +15,12 @@
## Purpose
A member test announces and reveals its result beside that exact member rather
than leaving feedback in the off-screen global footer. The normal primary
Test connection result stays in the footer; both remain endpoint-qualified
and do not update background or agent health. The member-section test and
browser interaction proof check the locally scoped live result.
### Proxmox endpoint trust in the existing connection editor
The primary SSL fingerprint edits only its authority/base path. Each peer shows
@@ -0,0 +1,59 @@
{
"version": 1,
"base_sha": "dfbe3fa91cd82693242da203b53f7161c7a61156",
"verified_at": "2026-10-27T09:01:05Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Settings/NodeModalClusterMembersSection.tsx",
"frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx",
"frontend-modern/src/components/Settings/useNodeModalState.ts"
],
"content_sha256": {
"frontend-modern/src/components/Settings/NodeModalClusterMembersSection.tsx": "81b2ff3c164ced6a5843b4cdd01031492c3f9b58209e7e0aed10c73fd6032222",
"frontend-modern/src/components/Settings/NodeModalStatusFooter.tsx": "0185405c945a69d5f47238df9095e17dc8890e19f31e4de36859d8884ace3224",
"frontend-modern/src/components/Settings/useNodeModalState.ts": "55228cde5c3e6e50d28c3f70fab7e8e850de4004f2364ce933f50270857eefe4"
},
"routes": [
"/settings/infrastructure"
],
"viewports": [
{
"width": 1365,
"height": 1000
},
{
"width": 390,
"height": 844
}
],
"states": [
"Separate primary and saved peer pins; preserved disabled collection flags",
"Qualified primary success while the peer still rejects its saved pin",
"Peer TLS failure and explicit pin update; save/authoritative readback/reopen",
"Explicit per-endpoint CA mode with verification enabled; empty policy saved/reopened",
"An old in-flight test result invalidated by a changed trust draft",
"Supplementary manually projected unknown-member state; deliberate CA choice, not silent pin clearing",
"Endpoint-bound success/failure feedback is beside the tested member and revealed within the modal viewport"
],
"interactions": [
"Focus and Enter on the peer test control",
"Edit independently verified synthetic pin values; Test connection and Test TLS for beta",
"Save changes and reopen the actual configured-node owner after GET readback",
"Clear only selected primary/peer pins with Verify SSL still enabled",
"Change a peer draft while an earlier successful response is held, then release it",
"Use CA validation for beta explicitly; no generated credentials or agent action",
"Observe peer failure within the viewport without scrolling; edit draft and invalidate the prior member success"
],
"checks": 96,
"browsers": {
"playwright": "1.56.1",
"desktop-chromium": "141.0.7390.37",
"narrow-webkit": "26.0"
},
"evidence_scope": "Actual saved/reopened API node and probe wires from final runtime source through production NodeCredentialSlot and configured-node owner. Member feedback is local and live, not hidden in the global footer. Unknown-member supplemental state is explicitly manual. No native or agent recovery claim.",
"native_pixel_review": {
"captures": 16,
"all_opened": true,
"limits": "Source-produced saved/readback/probe wires; manual unknown-member supplement. Member results are inline, live and automatically revealed without moving focus. Every latest desktop and narrow capture was presented through the native image tool; no reporter TLS or installed/agent-health acceptance."
}
}
@@ -2,6 +2,7 @@ import { Component, For, Show, createMemo, createUniqueId } from 'solid-js';
import type { ClusterEndpoint } from '@/types/nodes';
import type { NodeModalProps } from '@/components/Settings/nodeModalModel';
import type { NodeModalState } from '@/components/Settings/useNodeModalState';
import { InlineNotice } from '@/components/shared/InlineNotice';
import { SectionHeader } from '@/components/shared/SectionHeader';
import { controlClass, formHelpText } from '@/components/shared/Form';
@@ -192,6 +193,33 @@ export const NodeModalClusterMembersSection: Component<NodeModalClusterMembersSe
>
Test TLS for {endpoint.nodeName}
</button>
<Show when={state.testResult()?.endpoint === endpoint.tlsEndpoint}>
<div
ref={(element) =>
queueMicrotask(() => {
if (element.isConnected)
element.scrollIntoView?.({ block: 'nearest' });
})
}
>
<InlineNotice
class="break-words"
tone={
state.testResult()?.status === 'error'
? 'danger'
: state.testResult()?.status === 'warning'
? 'warning'
: 'success'
}
role="status"
aria-label={`TLS test result for ${endpoint.nodeName}`}
aria-live="polite"
aria-atomic="true"
>
{state.testResult()?.message}
</InlineNotice>
</div>
</Show>
</div>
</Show>
</div>
@@ -31,7 +31,7 @@ export const NodeModalStatusFooter: Component<NodeModalStatusFooterProps> = (pro
return (
<>
<Show when={state.testResult()}>
<Show when={state.testResult() && !state.testResult()?.endpoint}>
{(() => {
const result = state.testResult();
logger.debug('Test result display', {
@@ -1,6 +1,7 @@
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import { createSignal, type Setter } from 'solid-js';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { NodesAPI } from '@/api/nodes';
import type { ClusterEndpoint, NodeConfig } from '@/types/nodes';
import { NodeModalClusterMembersSection } from '../NodeModalClusterMembersSection';
import type { NodeModalProps } from '../nodeModalModel';
@@ -73,6 +74,7 @@ describe('Cluster member connection diagnostics', () => {
afterEach(() => {
cleanup();
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('gives a refused online member a named native disclosure with its actual reason', () => {
@@ -205,4 +207,23 @@ describe('Cluster member connection diagnostics', () => {
}),
);
});
it('shows a member test result beside that member without requiring a trip to the footer', async () => {
vi.spyOn(NodesAPI, 'testExistingNode').mockResolvedValueOnce({
status: 'success',
message:
'Connected to this PVE endpoint. Other endpoints and background collection were not tested.',
});
renderMembers([
member({ tlsEndpoint: 'https://member.test:8006', fingerprint: 'verified-pin' }),
]);
fireEvent.click(screen.getByRole('button', { name: 'Test TLS for member' }));
await waitFor(() =>
expect(screen.getByRole('status', { name: 'TLS test result for member' })).toHaveTextContent(
'member: Connected to this PVE endpoint.',
),
);
expect(screen.getByRole('status', { name: 'TLS test result for member' })).toHaveTextContent(
'Other endpoints and background collection were not tested.',
);
});
});
@@ -1384,6 +1384,7 @@ describe('settings architecture guardrails', () => {
expect(nodeModalStateSource).toContain('buildClusterEndpointOverridesPayload(');
expect(nodeModalStateSource).toContain('buildClusterEndpointTrustOverridesPayload(');
expect(nodeModalStateSource).toContain('clusterEndpointTrustEdited');
expect(nodeModalStatusFooterSource).toContain('!state.testResult()?.endpoint');
expect(nodeModalModelSource).toContain('export const buildClusterEndpointOverridesPayload = (');
});
@@ -134,6 +134,7 @@ describe('connection tests with retained credentials', () => {
});
expect(NodesAPI.testConnection).not.toHaveBeenCalled();
expect(state.testResult()?.message).toContain('peer:');
expect(state.testResult()?.endpoint).toBe(peer.tlsEndpoint);
state.updateClusterEndpointFingerprint(peer.tlsEndpoint, 'new-pin');
expect(state.testResult()).toBeNull();
});
@@ -27,6 +27,7 @@ type NodeModalTestResult = {
message: string;
isCluster?: boolean;
warnings?: string[];
endpoint?: string;
};
const PROXMOX_SETUP_HOST_REQUIRED_MESSAGE = 'Proxmox setup host is required';
@@ -578,6 +579,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
(endpoint && (data.password || data.tokenValue))
) {
setTestResult({
endpoint: endpoint?.tlsEndpoint,
status: 'error',
message:
'Save changed addresses or credentials before testing this endpoint with stored credentials.',
@@ -600,6 +602,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
});
if (generation !== probeGeneration) return;
setTestResult({
endpoint: endpoint?.tlsEndpoint,
status: result.warnings?.length ? 'warning' : 'success',
message: `${endpoint ? endpoint.nodeName + ': ' : ''}${result.message || 'Endpoint connection successful; background collection was not tested.'}`,
warnings: result.warnings,
@@ -611,6 +614,7 @@ export const useNodeModalState = (props: NodeModalProps) => {
? error.message.replace(/^API request failed: \d{3}\s*/, '')
: 'Connection failed';
setTestResult({
endpoint: endpoint?.tlsEndpoint,
status: 'error',
message: `${endpoint ? endpoint.nodeName + ': ' : ''}${message}`,
});