From 56d51dd4615b0d9f945cc5c113272f5cdf7e06e8 Mon Sep 17 00:00:00 2001
From: martino <32328813+f3rs3n@users.noreply.github.com>
Date: Tue, 22 Sep 2026 18:59:50 +0200
Subject: [PATCH] fix(monitor): clarify BORG key guidance
---
.../tests/test_command_descriptions.py | 10 +
AppImage/components/host-backup.tsx | 13 +-
AppImage/messages/en/common.json | 6 +
tests/README-borg-ssh.md | 70 +++++++
tests/test_borg_ssh_browser.cjs | 177 ++++++++++++++++++
tests/test_borg_ssh_guidance.cjs | 121 ++++++++++++
6 files changed, 390 insertions(+), 7 deletions(-)
create mode 100644 tests/README-borg-ssh.md
create mode 100644 tests/test_borg_ssh_browser.cjs
create mode 100644 tests/test_borg_ssh_guidance.cjs
diff --git a/.github/scripts/tests/test_command_descriptions.py b/.github/scripts/tests/test_command_descriptions.py
index f86afd0c..873b2be0 100644
--- a/.github/scripts/tests/test_command_descriptions.py
+++ b/.github/scripts/tests/test_command_descriptions.py
@@ -115,6 +115,16 @@ class CommandDescriptionsTests(unittest.TestCase):
report["recommendations"].setdefault(
key, source_report["recommendations"][key])
path.write_text(json.dumps(temporary, ensure_ascii=False))
+ # Model steady-state generation for the six intentional English-only
+ # Borg additions, in temporary copies only. Keep every other gap visible.
+ if lang != "en":
+ copied = json.loads(path.read_text())
+ english = catalog("en")["backup"]
+ for key in ("sshUserHelpMessage", "sshKeyHelpMessage", "sshKeySetupTitle",
+ "sshKeySetupHelp", "sshAuthorizedKeyHelp"):
+ copied["backup"]["destinations"].setdefault(key, english["destinations"][key])
+ copied["backup"]["actions"].setdefault("prepareSshKey", english["actions"]["prepareSshKey"])
+ path.write_text(json.dumps(copied), encoding="utf-8")
before = {p: p.read_bytes() for p in root.glob("*/common.json")}
argv = [str(SCRIPT), "--source", str(root / "en/common.json"),
"--messages-dir", str(root), "--languages", languages, "--sleep", "0"]
diff --git a/AppImage/components/host-backup.tsx b/AppImage/components/host-backup.tsx
index 9226f0cd..2a071781 100644
--- a/AppImage/components/host-backup.tsx
+++ b/AppImage/components/host-backup.tsx
@@ -6435,7 +6435,7 @@ function AddDestinationDialog({
setBorgSshUser(e.target.value)} className="font-mono mt-1" placeholder="borg" />
- {t("backup.destinations.sshUserHelpBefore")} borg serve. {t("backup.destinations.sshUserHelpAfter")} borg, {t("backup.destinations.not")} root.
+ {t("backup.destinations.sshUserHelpMessage")}
@@ -6464,14 +6464,13 @@ function AddDestinationDialog({
setBorgSshKeyPath(e.target.value)} className="font-mono mt-1" />
- {t("backup.destinations.sshKeyHelpBefore")}
- {" "}{t("backup.destinations.sshKeyHelpMiddle")} {t("backup.actions.generateKey")} {t("backup.destinations.sshKeyHelpAfter")}
+ {t("backup.destinations.sshKeyHelpMessage", { action: t("backup.actions.prepareSshKey") })}
-
{t("backup.destinations.generateNewSshKey")}
+
{t("backup.destinations.sshKeySetupTitle")}
{generatedKey ? (
<>
- {t("backup.destinations.appendAuthorizedKeyBefore")} ~{borgSshUser}/.ssh/authorized_keys:
+ {t("backup.destinations.sshAuthorizedKeyHelp", { user: borgSshUser })}
) : (
- {t("backup.destinations.createsSshKeyBefore")} borg serve {t("backup.destinations.createsSshKeyAfter")}
+ {t("backup.destinations.sshKeySetupHelp")}
)}
diff --git a/AppImage/messages/en/common.json b/AppImage/messages/en/common.json
index 34ce516d..0d9c2e33 100644
--- a/AppImage/messages/en/common.json
+++ b/AppImage/messages/en/common.json
@@ -3820,6 +3820,7 @@
"enable": "Enable",
"format": "Format",
"generateKey": "Generate key",
+ "prepareSshKey": "Prepare key",
"import": "Import",
"importKeyfile": "Import keyfile",
"mount": "Mount",
@@ -3981,11 +3982,16 @@
"saveAnotherPbsTitle": "Save another PBS destination",
"shortIdentifierHelp": "Short name shown in Monitor.",
"single": "Destination",
+ "sshAuthorizedKeyHelp": "Review the line below before adding it to ~/.ssh/authorized_keys for {user} on the remote host.",
"sshKeyHelpAfter": "and allow it on the remote host.",
"sshKeyHelpBefore": "Monitor stores the private key at",
"sshKeyHelpMiddle": "Copy the public key to",
+ "sshKeyHelpMessage": "Private key path on this host. Use {action}, then review and add the displayed public-key line to the remote user's ~/.ssh/authorized_keys.",
+ "sshKeySetupHelp": "Creates a key at the specified local path if none exists; otherwise reads its public key. No key is installed on the remote host.",
+ "sshKeySetupTitle": "SSH key setup",
"sshUserHelpAfter": "for example root.",
"sshUserHelpBefore": "User on the remote host,",
+ "sshUserHelpMessage": "Remote account used to access the Borg repository, for example borg.",
"title": "Destinations",
"unmountTitle": "Unmount this destination",
"whereIsBorgRepo": "Where is the Borg repository?"
diff --git a/tests/README-borg-ssh.md b/tests/README-borg-ssh.md
new file mode 100644
index 00000000..a0e4c87d
--- /dev/null
+++ b/tests/README-borg-ssh.md
@@ -0,0 +1,70 @@
+# Borg SSH guidance checks
+
+These standalone checks are separate from the Python i18n suite. They do not
+contact a Proxmox host or generate SSH keys.
+
+## JSX/provider regression
+
+Prerequisites: Node **22.14+** and installed `AppImage` dependencies (including
+React, React DOM and TypeScript). From the repository root:
+
+```bash
+node tests/test_borg_ssh_guidance.cjs
+```
+
+The test extracts the actual SSH branch from `AddDestinationDialog`, renders it
+with React, and executes the actual provider's lookup/interpolation callback.
+Basic control wrappers replace shadcn components only in this fast test. It covers
+all shipped catalogs, unconditional missing-key English fallback, synthetic
+whole-message translations and reordered placeholders, initial/result/loading
+states, account escaping, blank/custom paths and literal public-key output.
+The optional `account` argument isolates the remote-account instruction.
+
+## Real-component browser fixture
+
+Additional prerequisites: `esbuild`, `playwright`, its Chromium browser and a
+successful full Monitor frontend build. The validation runtime used esbuild
+0.28.2 and Playwright 1.63.0. If these optional tools are not installed, install
+them locally without changing the project manifests/lockfile:
+
+```bash
+cd AppImage
+npm install --no-save --package-lock=false --legacy-peer-deps esbuild@0.28.2 playwright@1.63.0
+node node_modules/playwright/cli.js install chromium
+npm run build
+cd ..
+node tests/test_borg_ssh_browser.cjs /absolute/path/to/borg-browser-evidence
+```
+
+The fixture bundles the actual complete `AddDestinationDialog`, real shadcn
+components, real API helper and `I18nProvider`, with CSS from `AppImage/out` by default. Set `BORG_SSH_CSS_DIR` to the
+`_next/static/css` directory of an archived full build to reuse that build's CSS.
+A test-only in-memory export exposes the unexported dialog; production source is
+not rewritten. Browser locale fixtures omit the six new keys from Italian and
+supply expanded/reordered synthetic messages in German **in memory only**.
+They are not proposed translations.
+
+All requests are intercepted **before navigation**. Known static resources and
+read endpoints are fulfilled from fixtures; key preparation POSTs receive inert
+fresh/existing/error responses. All other requests, including Save, are aborted
+and make the run fail. No server is needed. The test checks current request
+payloads and characterizes unchanged last-response retention after field edits
+and errors; mocked fresh/existing responses are not a backend generation test.
+
+The matrix is desktop/mobile (1440×1000, 390×844), light/dark, English/forced
+Italian fallback/synthetic expansion. It writes initial/result screenshots and
+`browser-results.json`, and asserts message/button horizontal fit. The modal is
+scrolled to show the relevant panel; this is not whole-dashboard acceptance.
+
+## Other gates
+
+```bash
+python3 -m unittest discover -s .github/scripts/tests -v
+cd AppImage
+node node_modules/typescript/bin/tsc --noEmit --incremental false
+```
+
+Run typechecking separately: the production build skips it. Compare diagnostics
+against the unchanged baseline with the same dependencies; a successful build
+is not a clean typecheck. CONTRIBUTING's real-Proxmox deployment smoke test is a
+separate integration gate and is not replaced by these fixtures.
diff --git a/tests/test_borg_ssh_browser.cjs b/tests/test_borg_ssh_browser.cjs
new file mode 100644
index 00000000..139fb998
--- /dev/null
+++ b/tests/test_borg_ssh_browser.cjs
@@ -0,0 +1,177 @@
+// Isolated real-component browser fixture, not a Proxmox integration test.
+// Requires AppImage dependencies, esbuild, playwright + Chromium, and a completed frontend build.
+// Run: node tests/test_borg_ssh_browser.cjs /absolute/path/to/evidence-directory
+// All navigation/assets/API requests are intercepted BEFORE navigation; nothing reaches a host.
+const fs = require('node:fs');
+const path = require('node:path');
+const assert = require('node:assert/strict');
+const { createRequire } = require('node:module');
+const app = path.resolve(__dirname, '../AppImage');
+const req = createRequire(path.join(app, 'package.json'));
+const { build } = req('esbuild');
+const { chromium } = req('playwright');
+const out = path.resolve(process.argv[2] || 'borg-browser-evidence');
+fs.mkdirSync(out, { recursive: true });
+const en = JSON.parse(fs.readFileSync(path.join(app, 'messages/en/common.json')));
+const cssDir = process.env.BORG_SSH_CSS_DIR || path.join(app, 'out/_next/static/css');
+const css = fs.readdirSync(cssDir).filter(p => p.endsWith('.css')).map(p => fs.readFileSync(path.join(cssDir, p), 'utf8')).join('\n');
+const expanded = {
+ sshUserHelpMessage: 'FIXTURE REMOTE ACCOUNT: the account on the remote repository host, not a local command.',
+ sshKeyHelpMessage: '{action}: FIXTURE ACTION FIRST. Review the public-key line before adding it to the remote account authorized_keys file. The private key remains on this host.',
+ sshKeySetupTitle: 'FIXTURE SSH key setup',
+ sshKeySetupHelp: 'FIXTURE INITIAL: creates a missing local key, otherwise reads its public key. Nothing is installed on the remote host by this action.',
+ sshAuthorizedKeyHelp: '{user}: FIXTURE USER FIRST. Review the following public-key line before manually adding it to the remote account ~/.ssh/authorized_keys.',
+};
+const records = [], unexpected = [], errors = [], requests = [];
+(async () => {
+ const bundle = await build({
+ stdin: { contents: `import React from 'react';
+import {createRoot} from 'react-dom/client';
+import {I18nProvider} from './lib/i18n/provider';
+import {BorgFixtureDialog} from './components/host-backup';
+const editing = new URLSearchParams(location.search).has('new') ? null : {kind:'borg',name:'fixture',repository:'ssh://borg@backup.example.invalid/backup/repo',ssh_key_path:'/root/.ssh/proxmenux_borg',encrypt_mode:'none'};
+createRoot(document.getElementById('root')).render({}} onSaved={()=>{throw Error('Save must never be submitted')}} />);`,
+ resolveDir: app, sourcefile: 'borg-fixture.tsx', loader: 'tsx' },
+ bundle: true, write: false, outfile: 'fixture.js', format: 'iife', platform: 'browser', jsx: 'automatic',
+ define: { 'process.env.NODE_ENV': '"development"', 'process.env.NEXT_PUBLIC_API_PORT': '"8008"' },
+ plugins: [{ name: 'test-only-exports-and-synthetic-locale', setup(b) {
+ b.onLoad({ filter: /host-backup\.tsx$/ }, args => ({ contents: fs.readFileSync(args.path, 'utf8') + '\nexport { AddDestinationDialog as BorgFixtureDialog };', loader: 'tsx' }));
+ b.onLoad({ filter: /messages\/(de|it)\/common\.json$/ }, args => {
+ const locale = JSON.parse(fs.readFileSync(args.path));
+ if (args.path.endsWith('/de/common.json')) {
+ Object.assign(locale.backup.destinations, expanded);
+ locale.backup.actions.prepareSshKey = 'Prepare fixture key';
+ } else {
+ // Unconditional missing-key fixture survives future automated translations.
+ for (const key of Object.keys(expanded)) delete locale.backup.destinations[key];
+ delete locale.backup.actions.prepareSshKey;
+ }
+ return { contents: JSON.stringify(locale), loader: 'json' };
+ });
+ }}],
+ });
+ const javascript = bundle.outputFiles[0].text;
+ const browser = await chromium.launch({ headless: true });
+ try {
+ for (const viewport of [{ width: 1440, height: 1000 }, { width: 390, height: 844 }]) {
+ for (const theme of ['light', 'dark']) for (const language of ['en', 'it', 'de']) {
+ const id = `${viewport.width}-${theme}-${language}`;
+ const context = await browser.newContext({ viewport, colorScheme: theme, serviceWorkers: 'block' });
+ const page = await context.newPage();
+ page.on('pageerror', e => errors.push({ id, message: e.message }));
+ let release = null, calls = 0;
+ const fixtureLine = 'command="borg serve --restrict-to-path /backup/repo",restrict ssh-ed25519 AAAA-fixture-only \n';
+ // No route.continue() exists: unexpected traffic is aborted and fails the test.
+ await context.route('**/*', async route => {
+ const request = route.request(), url = new URL(request.url());
+ requests.push({ id, method: request.method(), url: request.url(), body: request.postData() });
+ if (url.origin === 'https://borg.fixture.invalid' && request.method() === 'GET') {
+ if (url.pathname === '/') return route.fulfill({ contentType: 'text/html', body: `` });
+ if (url.pathname === '/fixture.js') return route.fulfill({ contentType: 'application/javascript', body: javascript });
+ if (url.pathname === '/fixture.css') return route.fulfill({ contentType: 'text/css', body: css });
+ if (url.pathname === '/api/host-backups/destinations') return route.fulfill({ json: { pbs: [], borg: [], local: { entries: [] } } });
+ if (url.pathname === '/api/host-backups/usb-drives') return route.fulfill({ json: { drives: [] } });
+ }
+ if (url.origin === 'https://borg.fixture.invalid' && url.pathname === '/api/host-backups/ssh-keys/generate' && request.method() === 'POST') {
+ calls++;
+ const body = request.postDataJSON();
+ records.push({ id, fixture: calls === 1 ? 'fresh-key' : calls === 2 ? 'existing-key' : 'error', body });
+ await new Promise(resolve => { release = resolve; });
+ release = null;
+ if (calls === 3) return route.fulfill({ status: 500, json: { error: 'Fixture: public key unavailable' } });
+ return route.fulfill({ json: { public_key: 'ssh-ed25519 AAAA-fixture-only', authorized_keys_line: fixtureLine } });
+ }
+ unexpected.push({ id, method: request.method(), url: request.url() });
+ return route.abort('blockedbyclient');
+ });
+ await context.addInitScript(({ language }) => {
+ localStorage.setItem('proxmenux-ui-language', language);
+ }, { language });
+ await page.goto('https://borg.fixture.invalid/');
+ await page.waitForFunction(language => document.documentElement.lang === language, language);
+ assert.equal(await page.evaluate(() => document.documentElement.classList.contains('dark')), theme === 'dark');
+ await page.locator('#borgKeyPath').waitFor();
+ const action = language === 'de' ? 'Prepare fixture key' : 'Prepare key';
+ const button = page.getByRole('button', { name: action, exact: true });
+ await button.waitFor();
+ assert.equal(await page.locator('#borgKeyPath').inputValue(), '/root/.ssh/proxmenux_borg');
+ assert.ok((await page.locator('[role=dialog]').innerText()).includes(language === 'de' ? 'FIXTURE INITIAL' : en.backup.destinations.sshKeySetupHelp));
+ await button.evaluate(button => button.parentElement.parentElement.scrollIntoView({ block: 'center' }));
+ assert.equal(await button.evaluate(button => {
+ const panel = button.parentElement.parentElement;
+ return [panel, ...panel.querySelectorAll('p, button, span')].every(e => e.scrollWidth <= e.clientWidth + 1 || getComputedStyle(e).display === 'inline');
+ }), true, `${id}: initial setup guidance fits`);
+ await page.screenshot({ animations: 'disabled', path: path.join(out, `${id}-initial.png`) });
+ // Blank input remains disabled; no attempt is sent.
+ await page.locator('#borgKeyPath').fill('');
+ assert.equal(await button.isDisabled(), true);
+ assert.equal(calls, 0);
+ const longPath = '/fixture/long-local-private-key-directory/'.repeat(6) + 'key';
+ await page.locator('#borgKeyPath').fill(longPath);
+ await page.locator('#borgSshUser').fill('root');
+ await button.click();
+ await page.waitForFunction(() => document.querySelector('button svg.animate-spin'));
+ assert.equal(await button.isDisabled(), true);
+ assert.equal(await button.innerText(), action);
+ assert.ok(release, 'intercepted mock POST is pending');
+ release();
+ await page.locator('textarea[readonly]').waitFor();
+ assert.equal(await page.locator('textarea[readonly]').inputValue(), fixtureLine);
+ assert.ok((await page.locator('[role=dialog]').innerText()).includes(language === 'de' ? 'root: FIXTURE USER FIRST' : 'for root on the remote host.'));
+ // Changing input preserves the last response: characterize, do not change stale-response behavior.
+ await page.locator('#borgKeyPath').fill('/fixture/already-existing-private-key');
+ await page.locator('#borgSshUser').fill('');
+ assert.equal(await page.locator('textarea[readonly]').inputValue(), fixtureLine);
+ await button.click();
+ await page.waitForFunction(() => document.querySelector('button svg.animate-spin'));
+ assert.equal(await button.innerText(), action);
+ release();
+ await page.waitForFunction(() => !document.querySelector('button svg.animate-spin'));
+ assert.equal(await page.locator('textarea[readonly]').inputValue(), fixtureLine);
+ assert.equal(await page.locator('archive-user').count(), 0);
+ assert.ok((await page.locator('[role=dialog]').innerText()).includes(''));
+ await button.evaluate(button => button.parentElement.parentElement.scrollIntoView({ block: 'center' }));
+ // Measure the changed guidance, its action, and result area, not unrelated whole-dialog layout.
+ const layout = await button.evaluate(button => {
+ const panel = button.parentElement.parentElement;
+ const help = document.querySelector('#borgKeyPath').parentElement.querySelector('p');
+ const userHelp = document.querySelector('#borgSshUser').parentElement.querySelector('p');
+ const elements = [panel, button.parentElement, button, help, userHelp, panel.querySelector('p'), panel.querySelector('textarea')];
+ return elements.map(e => {
+ const r = e.getBoundingClientRect();
+ return { tag: e.tagName, text: e.tagName === 'TEXTAREA' ? '[fixture public-key line]' : e.textContent, client: e.clientWidth, scroll: e.scrollWidth, left: r.left, right: r.right, viewport: innerWidth };
+ });
+ });
+ for (const e of layout.filter(e => e.tag !== 'TEXTAREA')) {
+ assert.ok(e.scroll <= e.client + 1, `${id}: guidance overflow ${JSON.stringify(e)}`);
+ assert.ok(e.left >= 0 && e.right <= viewport.width + 1, `${id}: guidance outside viewport`);
+ }
+ await page.screenshot({ animations: 'disabled', path: path.join(out, `${id}-result.png`) });
+ records.push({ id, fixture: 'layout', layout });
+ await button.click();
+ await page.waitForFunction(() => document.querySelector('button svg.animate-spin'));
+ release();
+ await page.getByText('Fixture: public key unavailable', { exact: true }).waitFor();
+ assert.equal(await button.isEnabled(), true);
+ assert.equal(await button.innerText(), action);
+ assert.equal(await page.locator('textarea[readonly]').inputValue(), fixtureLine);
+ assert.equal(calls, 3);
+ assert.deepEqual(records.filter(r => r.id === id && r.body).map(r => r.body), [
+ { key_path: longPath, remote_path: '/backup/repo' },
+ { key_path: '/fixture/already-existing-private-key', remote_path: '/backup/repo' },
+ { key_path: '/fixture/already-existing-private-key', remote_path: '/backup/repo' },
+ ], 'real component submits the current path, not a fixed path or the previous response');
+ await context.close();
+ }
+ }
+ assert.deepEqual(unexpected, []);
+ assert.deepEqual(errors, []);
+ const posts = requests.filter(r => r.method === 'POST');
+ assert.equal(posts.length, 36);
+ assert.ok(posts.every(p => p.url.endsWith('/ssh-keys/generate')));
+ console.log('PASS: 12 real-component/provider browser scenarios; desktop/mobile × light/dark × English/Italian fallback/synthetic expanded translation; 36 mocked POSTs, 0 real API calls.');
+ } finally {
+ fs.writeFileSync(path.join(out, 'browser-results.json'), JSON.stringify({ records, requests, unexpected, errors }, null, 2));
+ await browser.close();
+ }
+})().catch(e => { console.error(e); process.exitCode = 1; });
diff --git a/tests/test_borg_ssh_guidance.cjs b/tests/test_borg_ssh_guidance.cjs
new file mode 100644
index 00000000..492ebbc1
--- /dev/null
+++ b/tests/test_borg_ssh_guidance.cjs
@@ -0,0 +1,121 @@
+// Run: node tests/test_borg_ssh_guidance.cjs [account|setup]
+// Prerequisites: Node 22.14+, installed AppImage dependencies (React + TypeScript).
+// Renders the actual SSH JSX branch and executes the actual provider lookup callback.
+// No host-management imports, network, backend, or real SSH key generation.
+const assert = require('node:assert/strict');
+const fs = require('node:fs');
+const path = require('node:path');
+const { createRequire } = require('node:module');
+const app = path.resolve(__dirname, '../AppImage');
+const req = createRequire(path.join(app, 'package.json'));
+const ts = req('typescript');
+const React = req('react');
+const { renderToStaticMarkup } = req('react-dom/server');
+const read = p => fs.readFileSync(path.join(app, p), 'utf8');
+const parse = (s, name = 'fixture.tsx') => ts.createSourceFile(name, s, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
+function nodes(tree, predicate) {
+ const result = [];
+ function visit(n) { if (predicate(n)) result.push(n); ts.forEachChild(n, visit); }
+ visit(tree); return result;
+}
+function evaluate(source, bindings) {
+ const js = ts.transpileModule(source, { compilerOptions: {
+ jsx: ts.JsxEmit.ReactJSX, module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020,
+ }}).outputText;
+ const module = { exports: {} };
+ // Only trusted checked-out source is compiled; fixture values are bindings, never code.
+ new Function('require', 'module', 'exports', ...Object.keys(bindings), js)(req, module, module.exports, ...Object.values(bindings));
+ return module.exports;
+}
+const source = read('components/host-backup.tsx');
+const tree = parse(source);
+const dialog = nodes(tree, n => ts.isFunctionDeclaration(n) && n.name?.text === 'AddDestinationDialog')[0];
+assert.ok(dialog, 'actual destination component exists');
+const unwrap = n => ts.isParenthesizedExpression(n) ? unwrap(n.expression) : n;
+const branch = nodes(dialog, n => ts.isConditionalExpression(n) && n.condition.getText(tree) === 'borgMode === "local"' && ts.isJsxFragment(unwrap(n.whenFalse)));
+assert.equal(branch.length, 1, 'unique actual Borg SSH JSX branch');
+const providerSource = read('lib/i18n/provider.tsx');
+const providerTree = parse(providerSource);
+const helpers = nodes(providerTree, n => ts.isFunctionDeclaration(n) && ['getMessage', 'interpolate'].includes(n.name?.text)).map(n => n.getText(providerTree)).join('\n');
+const callback = nodes(providerTree, n => ts.isVariableDeclaration(n) && n.name.getText(providerTree) === 't')[0].initializer.arguments[0].getText(providerTree);
+const en = JSON.parse(read('messages/en/common.json'));
+function translate(locale) {
+ return evaluate(`${helpers}\nmodule.exports = ${callback}`, { MESSAGE_CATALOG: { en, fixture: locale }, language: 'fixture' });
+}
+const noop = () => {};
+const component = tag => ({ children, ...props }) => React.createElement(tag, props, children);
+const literalLine = 'command="borg serve --restrict-to-path /backup/repo",restrict ssh-ed25519 AAAA-fixture-only \n';
+let renderCount = 0;
+function render(locale, { user = 'borg', generated = false, loading = false, keyPath = '/root/.ssh/proxmenux_borg' } = {}) {
+ renderCount++;
+ const bindings = {
+ t: translate(locale), borgSshUser: user, borgSshHost: 'backup.example.invalid', borgSshPort: '22',
+ borgSshRemotePath: '/backup/repo', borgSshKeyPath: keyPath,
+ generatedKey: generated ? { authorized_keys_line: literalLine, public_key: 'ssh-ed25519 AAAA-fixture-only' } : null,
+ generatingKey: loading, generateBorgKey: noop,
+ setBorgSshUser: noop, setBorgSshHost: noop, setBorgSshPort: noop, setBorgSshRemotePath: noop, setBorgSshKeyPath: noop,
+ Label: component('label'), Input: component('input'), Button: ({ size, variant, ...p }) => React.createElement('button', p),
+ Loader2: component('svg'), Plus: component('svg'),
+ };
+ const Fixture = evaluate(`module.exports = function Fixture() { return (${branch[0].whenFalse.getText(tree)}) }`, bindings);
+ return renderToStaticMarkup(React.createElement(Fixture));
+}
+const escape = value => value.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
+const account = 'Remote account used to access the Borg repository, for example borg.';
+const keyHelp = "Private key path on this host. Use Prepare key, then review and add the displayed public-key line to the remote user's ~/.ssh/authorized_keys.";
+const setupHelp = 'Creates a key at the specified local path if none exists; otherwise reads its public key. No key is installed on the remote host.';
+// Accept future shipped translations, while the always-empty synthetic locale
+// below continues to assert the independently specified English contract.
+const localText = (locale, group, key, fallback) => typeof locale.backup?.[group]?.[key] === 'string' ? locale.backup[group][key] : fallback;
+const keys = ['sshUserHelpMessage', 'sshKeyHelpMessage', 'sshKeySetupTitle', 'sshKeySetupHelp', 'sshAuthorizedKeyHelp'];
+
+const locales = fs.readdirSync(path.join(app, 'messages')).filter(l => fs.existsSync(path.join(app, 'messages', l, 'common.json')));
+// The empty synthetic locale is unconditional: fallback stays covered after upstream translations arrive.
+for (const locale of [{}, ...locales.map(l => JSON.parse(read(`messages/${l}/common.json`)))]) {
+ for (const user of ['borg', 'root', 'archive-user', '
']) {
+ const html = render(locale, { user });
+ assert.ok(html.includes(escape(localText(locale, 'destinations', 'sshUserHelpMessage', account))), 'remote-account guidance must not confuse borg serve with an account or prohibit root');
+ }
+}
+const synthetic = { backup: { destinations: { sshUserHelpMessage: 'REMOTE ACCOUNT FIXTURE' } } };
+assert.ok(render(synthetic).includes('REMOTE ACCOUNT FIXTURE'), 'actual JSX consumes translated whole account message');
+if (process.argv[2] !== 'account') {
+ for (const locale of [{}, ...locales.map(l => JSON.parse(read(`messages/${l}/common.json`)))]) {
+ for (const user of ['borg', 'root', 'archive-user', '
']) {
+ for (const generated of [false, true]) for (const loading of [false, true]) {
+ const html = render(locale, { user, generated, loading, keyPath: '/custom/long-local-path/'.repeat(8) + 'private_key' });
+ const action = localText(locale, 'actions', 'prepareSshKey', 'Prepare key');
+ const expectedKeyHelp = localText(locale, 'destinations', 'sshKeyHelpMessage', keyHelp.replace('Prepare key', '{action}')).replaceAll('{action}', action);
+ assert.ok(html.includes(escape(expectedKeyHelp)), 'complete local-path and manual-install instruction');
+ assert.ok(html.includes(escape(localText(locale, 'destinations', 'sshKeySetupTitle', 'SSH key setup'))));
+ assert.ok(html.match(/