diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 4408a7f5c..5fb7266f5 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,32 +1,16 @@ { "version": 1, - "base_sha": "ef460aa6542a41728e2fe3e4a8266488c919b0be", - "verified_at": "2026-09-02T18:04:36Z", + "base_sha": "54d58a74062ffb15c14510a24223f21fa6ec2fd3", + "verified_at": "2026-09-03T20:15:00Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx", - "frontend-modern/src/components/Settings/Settings.tsx", - "frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx", - "frontend-modern/src/components/WhatsNewCard.tsx", - "frontend-modern/src/i18n/messages.de.ts", - "frontend-modern/src/i18n/messages.es.ts", - "frontend-modern/src/i18n/messages.ts", - "frontend-modern/src/utils/localStorage.ts" + "frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx" ], "content_sha256": { - "frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx": "9512529c0e6b86ba4e1055c5009158d442bd8d4a84c2dd9f73822a053989886a", - "frontend-modern/src/components/Settings/Settings.tsx": "72682c86dd055761cbd68d4f57b684afe4d978a0b53748859d0721e711ce321a", - "frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx": "7f8ddc0c826b5abe90f40ac08ef3856d046510a7f72afe2f40f32ae72e547eba", - "frontend-modern/src/components/WhatsNewCard.tsx": "90543f19d61e9ba6d93251a7bcc9c179fa8d7ccb6ab3364e364d0ea13b2c31f0", - "frontend-modern/src/i18n/messages.de.ts": "f43d3c32d59f32178531e11a56e6014d384831871bb8cc3b472fca8e20a96840", - "frontend-modern/src/i18n/messages.es.ts": "928c7c7b34dd75ffd00639cb8b27ad81c0e6c8210db8ff41c234a26c5b9c9031", - "frontend-modern/src/i18n/messages.ts": "fef88e4096b8d49dc8e4a5b721804bb3b602dddef442827f0b315bec0d45a435", - "frontend-modern/src/utils/localStorage.ts": "19a532f55271b2fc696e3ba4ae6ba098e759f0f9df207015c803f7fcc0050bd6" + "frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx": "186324f5ede149574cebe2d758b4984077c9105c5ae926721e262cc26a5e2502" }, "routes": [ - "/ (first-run setup wizard, fresh data dir)", - "/settings/system-general#usage-telemetry (mock data, existing-install localStorage baseline)", - "/docs/PRIVACY (shipped privacy document)" + "/settings/infrastructure (managed local backend with deterministic Proxmox connection-ledger and node fixtures)" ], "viewports": [ { @@ -39,21 +23,19 @@ } ], "states": [ - "Setup welcome step: Usage statistics card, purpose-first copy, never-sold statement, \"you choose on the next step\", no PULSE_TELEMETRY instruction", - "Setup security step (bootstrap validation and quick-setup responses intercepted by Playwright, no real token): Usage statistics toggle default on, description leads with the benefit and carries the never-sold statement", - "Setup security step: toggle off, aria-pressed false; completion reached after Create Account", - "Settings system-general with prior release baseline: no telemetry-payload-update-notice banner rendered", - "Settings Usage data and privacy panel: description leads with purpose, carries the never-sold statement, Preview payload rendered as primary Button, Reset ID secondary", - "Settings panel with payload preview open at 390px: no horizontal page overflow", - "Shipped /docs/PRIVACY renders the \"What it is not used for\" section and the Payload changes table", - "Locales: en verified live; de/es catalog copy covered by i18n and SetupWizard localization unit tests" + "Manage Proxmox VE dialog with Username & Password selected and an unsaved username at desktop width", + "Desktop dialog after a 15-second connection-ledger poll replaced lastSeen and endpoint: live description updated, local endpoint and unsaved username remained unchanged", + "Manage Proxmox VE dialog with a distinct unsaved username at narrow width", + "Narrow dialog after a second 15-second ledger replacement: live description updated, local endpoint and unsaved username remained unchanged", + "Scrollable dialog body with visible header and close control, reachable Save changes footer, and no document-width overflow at both widths", + "Closed dialog with focus returned to the originating Manage button at both widths" ], "interactions": [ - "Fill bootstrap token field with a placeholder and click Verify bootstrap token (validation endpoint intercepted)", - "Click Usage statistics toggle on the security step", - "Click Create Account & Continue: observed POST /api/security/quick-setup then POST /api/system/settings/update {\"telemetryEnabled\":false}", - "Click Preview payload in Settings and read the rendered heartbeat JSON", - "Click Full details from the Settings panel and read the popup document", - "Resize to 390x844 and repeat the wizard, security step, and Settings panel checks" + "Opened Manage for a configured Proxmox VE source", + "Selected Username & Password and entered a non-secret unsaved username", + "Waited for the real 15-second ledger poll and observed endpoint/lastSeen replacement without form remount or value loss", + "Scrolled to and verified the Save changes footer remained reachable, then returned to the edited field", + "Pressed Escape and verified dialog dismissal and focus return to Manage", + "Repeated the complete edit, poll, scroll, dismissal, and focus-return journey at 390x844" ] } diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index d9fb77e75..7d9f2847c 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -7,6 +7,7 @@ import { createMemo, createSignal, onCleanup, + untrack, } from 'solid-js'; import { useLocation, useNavigate } from '@solidjs/router'; import X from 'lucide-solid/icons/x'; @@ -711,7 +712,14 @@ const InfrastructureWorkspaceContent: Component = onSaved: () => void; }) => { if (context.mode === 'edit') { - const connection = editingConnection(); + // Connection-ledger polls replace otherwise identical connection + // snapshots (for example when lastSeen advances). Do not make the + // credential-slot owner depend on those snapshots: recreating the slot + // discards its local form state before the operator can save. The + // selected source identity remains owned by editingRowSource until this + // flow closes, while the live row and attached-agent details continue + // to update elsewhere in the dialog. + const connection = untrack(editingConnection); if (!connection) { return (
({ pathname: '/settings/infrastructure', search: '', })); -const connectionState = vi.hoisted(() => ({ - connections: [] as Connection[], - rows: null as InfrastructureSystemRow[] | null, -})); +const connectionState = vi.hoisted(() => { + let connections: Connection[] = []; + let observe = () => {}; + let notifyChanged = () => {}; + return { + get connections() { + observe(); + return connections; + }, + set connections(value: Connection[]) { + connections = value; + }, + rows: null as InfrastructureSystemRow[] | null, + setObserver(nextObserve: () => void, nextNotifyChanged: () => void) { + observe = nextObserve; + notifyChanged = nextNotifyChanged; + }, + notifyChanged: () => notifyChanged(), + }; +}); +const nodeCredentialSlotState = vi.hoisted(() => ({ mountCount: 0 })); const emptyFleetRow = vi.hoisted( () => ({ @@ -169,11 +187,14 @@ vi.mock('../InfrastructureInstallerSection', () => ({ })); vi.mock('../ConnectionEditor/CredentialSlots/NodeCredentialSlot', () => ({ - NodeCredentialSlot: (props: { nodeType: string }) => ( -
- proxmox -
- ), + NodeCredentialSlot: (props: { nodeType: string }) => { + nodeCredentialSlotState.mountCount += 1; + return ( +
+ +
+ ); + }, })); vi.mock('../ConnectionEditor/CredentialSlots/TrueNASCredentialSlot', () => ({ @@ -287,6 +308,11 @@ describe('InfrastructureWorkspace', () => { routeState.search = ''; connectionState.connections = [connectionFixture()]; connectionState.rows = null; + connectionState.setObserver( + () => {}, + () => {}, + ); + nodeCredentialSlotState.mountCount = 0; Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, @@ -1002,6 +1028,35 @@ describe('InfrastructureWorkspace', () => { await waitFor(() => expect(manageButton).toHaveFocus()); }); + it('keeps the mounted node editor across connection-ledger refreshes', async () => { + const [revision, setRevision] = createSignal(0); + connectionState.setObserver(revision, () => setRevision((value) => value + 1)); + renderWorkspace({ + pveNodes: () => [{ id: 'pve-0', type: 'pve', name: 'zeus' } as any], + }); + + fireEvent.click(await screen.findByRole('button', { name: /^Manage$/i })); + const method = await screen.findByRole('textbox', { name: 'Authentication method' }); + fireEvent.input(method, { target: { value: 'agent' } }); + expect(method).toHaveValue('agent'); + + connectionState.connections = [ + connectionFixture({ + address: 'https://10.0.0.2:8006', + lastSeen: new Date(Date.now() + 15_000).toISOString(), + }), + ]; + connectionState.notifyChanged(); + + await waitFor(() => + expect(screen.getByRole('textbox', { name: 'Authentication method' })).toHaveValue('agent'), + ); + expect(screen.getByRole('dialog')).toHaveAccessibleDescription( + 'Proxmox VE ยท https://10.0.0.2:8006', + ); + expect(nodeCredentialSlotState.mountCount).toBe(1); + }); + it('shows standalone agent identity in the landing row and the agent detail drawer', async () => { const towerAgent = connectionFixture({ id: 'agent:tower',