Restore reviewed configuration recovery guidance on current main

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-10-25 10:01:14 +00:00
5 changed files with 162 additions and 6 deletions
@@ -10003,6 +10003,17 @@ there.
### Configuration transfers retain their confirmed workspace
Recovery's overview identifies the selected-workspace configuration export as
limited rather than a full-server backup before either dialog is opened. It
names Proxmox connections, discloses missing history, TrueNAS/vSphere, local
login credentials and agent enrolment state, and links the shipped migration
guide for complete scope and full-state recovery. Restore warns to back up the
destination and describes replacement of included settings and API-token records,
never a merge option. `RecoverySettingsPanel.test.tsx` covers the visible scope,
existing dialog actions and passphrase defaults; desktop/narrow browser proof
must inspect the overview and follow both actions into the production dialogs.
Archive formats, server application and access authority do not change.
Recovery's export/import dialogs capture and display the effective workspace ID
when opened, including explicit `default` scope. Their production flow freezes
inputs before asynchronous file reads, binds both POSTs through the API client's
@@ -5591,12 +5591,14 @@
"match_prefixes": [],
"match_files": [
"frontend-modern/src/components/Settings/BackupTransferDialogs.tsx",
"frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx",
"frontend-modern/src/components/Settings/useBackupTransferFlow.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/components/Settings/__tests__/BackupTransferDialogs.test.tsx",
"frontend-modern/src/components/Settings/__tests__/RecoverySettingsPanel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/useBackupTransferFlow.test.ts"
]
},
@@ -5772,7 +5774,6 @@
"frontend-modern/src/components/Settings/networkSettingsModel.ts",
"frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx",
"frontend-modern/src/components/Settings/OperationsPanel.tsx",
"frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx",
"frontend-modern/src/components/Settings/SecurityAuthPanel.tsx",
"frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx",
"frontend-modern/src/components/Settings/Settings.tsx",
@@ -0,0 +1,46 @@
{
"version": 1,
"base_sha": "5a0549a65dff152d75b77bb4a35d6a7ebda48493",
"verified_at": "2026-10-25T09:02:28Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx": "d958e3255357bed6e0f99481695c1d805b2143076f5678d0b37a97483bd83227"
},
"routes": [
"/settings/system/recovery (offline fixture mounting production RecoverySettingsPanel, useBackupTransferFlow, BackupTransferDialogs, API client and CSS; not full-app auth)",
"/docs/MIGRATION (production Docs viewer fetching exact shipped markdown in a separate tab)"
],
"states": [
"Configuration-only selected-workspace scope and exclusions visible before opening either dialog. Proxmox-specific connection wording; replacement of included settings/API-token records, no merge promise.",
"Overview wraps at desktop/320px in light and dark themes. Existing export/import dialogues keep captured workspace, masked inputs and scope guidance."
],
"interactions": [
"Desktop keyboard and narrow touch open the existing migration guide in a separate tab without losing Recovery.",
"Open existing export through real flow; enter masked synthetic passphrase and explicitly download the synthetic server response with original scoped API headers.",
"Open existing restore, select synthetic file and passphrase, inspect replacement warning, cancel without POST, and return focus to Restore Backup.",
"44 checks pass; all ten captures opened using native view_image. Playwright 1.56.1 and Chromium 141.0.7390.37."
],
"viewports": [
{
"width": 1440,
"height": 1000,
"browser": "Chromium",
"browser_version": "141.0.7390.37"
},
{
"width": 320,
"height": 940,
"touch": true,
"browser": "Chromium",
"browser_version": "141.0.7390.37"
}
],
"evidence": "Current lane RECEIPTS.md, browser/browser-result.json, PIXEL-REVIEW.md and BROWSER-BINDING.json. Build/fixture/driver and synthetic downloads retained as evidence only.",
"limitations": [
"Safe synthetic HTTP uses real frontend producer/consumer, not server auth, cryptography, import application/idempotency, full app shell, private/native recovery or release delivery.",
"Full-document narrow guide capture is overview evidence, not exhaustive typography acceptance. No Back/reload or excluded reader/renderer/native scenario was attempted."
]
}
@@ -2,6 +2,8 @@ import { Component, Show, For, Accessor, Setter, createUniqueId } from 'solid-js
import SettingsPanel from '@/components/shared/SettingsPanel';
import { FormSelect } from '@/components/shared/FormSelect';
import { SectionHeader } from '@/components/shared/SectionHeader';
import { ExternalTextLink } from '@/components/shared/ExternalTextLink';
import { getShippedDocUrl } from '@/utils/docsLinks';
import {
BACKUP_INTERVAL_MAX_MINUTES,
BACKUP_INTERVAL_OPTIONS,
@@ -209,11 +211,19 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
{/* Backup & Restore Section */}
<div class="p-4 sm:p-6">
<SectionHeader
title="Backup & restore"
description="Backup your node configurations and credentials or restore from a previous backup."
title="Configuration backup & restore"
description="Create an encrypted configuration export for the selected workspace, not a full-server backup."
size="md"
class="mb-4"
/>
<p class="mb-4 text-sm text-muted">
History, TrueNAS/vSphere connections, local login credentials and agent enrolment state
are not included. Check the{' '}
<ExternalTextLink href={getShippedDocUrl('MIGRATION.md')}>
migration guide
</ExternalTextLink>{' '}
for the full scope and full-state recovery.
</p>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Export Section */}
@@ -253,7 +263,7 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
clip-rule="evenodd"
/>
</svg>
<span>All node connections & credentials</span>
<span>Proxmox connections & credentials</span>
</li>
<li class="flex items-center gap-2 text-xs text-muted">
<svg
@@ -331,7 +341,9 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
<h4 class="text-base font-semibold text-base-content mb-1">
Restore Configuration
</h4>
<p class="text-sm text-muted mb-3">Import from an encrypted backup file</p>
<p class="text-sm text-muted mb-3">
Import from an encrypted configuration file. Back up the destination first.
</p>
{/* Feature list */}
<ul class="space-y-1 mb-4">
@@ -343,7 +355,7 @@ export const RecoverySettingsPanel: Component<RecoverySettingsPanelProps> = (pro
clip-rule="evenodd"
/>
</svg>
<span>Merge or replace existing config</span>
<span>Replaces included settings and API-token records</span>
</li>
<li class="flex items-center gap-2 text-xs text-muted">
<svg class="w-3.5 h-3.5 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
@@ -0,0 +1,86 @@
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createSignal } from 'solid-js';
import { RecoverySettingsPanel } from '../RecoverySettingsPanel';
function renderRecovery(hasAuthentication = false) {
const [enabled, setEnabled] = createSignal(true);
const [interval, setInterval] = createSignal(300);
const [minutes, setMinutes] = createSignal(5);
const [custom, setCustom] = createSignal(false);
const [, setUnsaved] = createSignal(false);
const setShowExportDialog = vi.fn();
const setShowImportDialog = vi.fn();
const setUseCustomPassphrase = vi.fn();
render(() => (
<RecoverySettingsPanel
backupPollingEnabled={enabled}
setBackupPollingEnabled={setEnabled}
backupPollingInterval={interval}
setBackupPollingInterval={setInterval}
backupPollingCustomMinutes={minutes}
setBackupPollingCustomMinutes={setMinutes}
backupPollingUseCustom={custom}
setBackupPollingUseCustom={setCustom}
backupPollingEnvLocked={() => false}
backupIntervalSelectValue={() => String(interval())}
backupIntervalSummary={() => 'Every 5 minutes'}
setHasUnsavedChanges={setUnsaved}
showExportDialog={() => false}
setShowExportDialog={setShowExportDialog}
showImportDialog={() => false}
setShowImportDialog={setShowImportDialog}
setUseCustomPassphrase={setUseCustomPassphrase}
securityStatus={() => ({ hasAuthentication })}
/>
));
return { setShowExportDialog, setShowImportDialog, setUseCustomPassphrase };
}
describe('RecoverySettingsPanel', () => {
afterEach(cleanup);
it('discloses the limited archive scope before either transfer is opened', () => {
renderRecovery();
const text = document.body.textContent;
expect(text).toContain('selected workspace, not a full-server backup');
expect(text).toContain('Proxmox connections & credentials');
expect(text).toContain(
'History, TrueNAS/vSphere connections, local login credentials and agent enrolment state are not included',
);
expect(text).not.toContain('All node connections');
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
const guide = screen.getByRole('link', { name: 'migration guide' });
expect(guide).toHaveAttribute('href', '/docs/MIGRATION');
expect(guide).toHaveAttribute('target', '_blank');
expect(guide).toHaveAttribute('rel', 'noopener noreferrer');
});
it('warns that restoring replaces included settings rather than offering a merge', () => {
renderRecovery(true);
const text = document.body.textContent;
expect(text).toContain('Back up the destination first');
expect(text).toContain('Replaces included settings and API-token records');
expect(text).not.toContain('Merge or replace');
expect(screen.getByRole('button', { name: 'Restore Backup' })).toBeEnabled();
});
it.each([false, true])(
'opens export with the existing passphrase default (authentication: %s)',
(hasAuthentication) => {
const actions = renderRecovery(hasAuthentication);
fireEvent.click(screen.getByRole('button', { name: 'Create Backup' }));
expect(actions.setUseCustomPassphrase).toHaveBeenCalledExactlyOnceWith(!hasAuthentication);
expect(actions.setShowExportDialog).toHaveBeenCalledExactlyOnceWith(true);
expect(actions.setShowImportDialog).not.toHaveBeenCalled();
},
);
it('opens only the existing import dialog from Restore Backup', () => {
const actions = renderRecovery();
fireEvent.click(screen.getByRole('button', { name: 'Restore Backup' }));
expect(actions.setShowImportDialog).toHaveBeenCalledExactlyOnceWith(true);
expect(actions.setShowExportDialog).not.toHaveBeenCalled();
expect(actions.setUseCustomPassphrase).not.toHaveBeenCalled();
});
});