mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-28 06:27:26 +00:00
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:
@@ -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', {
|
||||
|
||||
+22
-1
@@ -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 = (');
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -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}`,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user