mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Localize first-session monitoring journey
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
# Localized Product Surfaces First Session Monitoring - 2026-06-14
|
||||
|
||||
## Slice
|
||||
|
||||
- Claimed governed slice: `candidate-lane:localized-product-surfaces`.
|
||||
- Migrated journey: first-session setup through the first monitoring handoff, including Runtime Home loading copy, the Setup Wizard shell and progress indicator, Welcome bootstrap-token guidance, Security admin-account creation copy, Setup Completion credential/next-step/source-choice copy, and the connected first monitored system handoff.
|
||||
- First-wave locales: German (`de`) and Spanish (`es`).
|
||||
|
||||
## Evidence
|
||||
|
||||
- `frontend-modern/src/i18n/messages.ts` now includes explicit German and Spanish catalog entries for the migrated first-session monitoring journey.
|
||||
- `frontend-modern/src/i18n/policy.ts` records first-session monitoring non-translation tokens for commands, URLs, API labels, generated credentials, product/source identifiers, and setup-token file names.
|
||||
- `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`, `StepIndicator.tsx`, `steps/WelcomeStep.tsx`, `steps/SecurityStep.tsx`, `SetupCompletionPanel.tsx`, and `setupCompletionModel.ts` now render migrated operator copy through the i18n owner layer instead of page-local English.
|
||||
- `frontend-modern/src/pages/RuntimeHome.tsx` now renders the authenticated workspace handoff through the same catalog.
|
||||
- Guardrails cover catalog completeness, explicit first-wave translations, unsupported-locale fallback through the shared i18n foundation, machine-facing token preservation, localized render proof for German and Spanish setup states, and source checks that fail if migrated first-session copy returns to hardcoded English in the touched surfaces.
|
||||
|
||||
## Verification
|
||||
|
||||
- `npm --prefix frontend-modern test -- --run src/i18n/__tests__/i18n.test.ts src/components/SetupWizard/__tests__/SetupWizard.localization.test.tsx src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts src/components/SetupWizard/__tests__/WelcomeStep.test.tsx src/components/SetupWizard/__tests__/SetupCompletionPanel.test.tsx src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx src/components/SetupWizard/__tests__/SetupWizard.test.tsx src/pages/__tests__/RuntimeHome.test.tsx` passed: 8 files, 44 tests.
|
||||
- `npm --prefix frontend-modern test -- --run src/components/SetupWizard/__tests__/SetupWizard.localization.test.tsx src/i18n/__tests__/i18n.test.ts` passed: 2 files, 18 tests.
|
||||
- `npm --prefix frontend-modern run type-check` passed.
|
||||
- Browser proof passed against the managed dev server at `http://127.0.0.1:5173`: `/settings/system-general` switched the app language through the visible `Español` and `Deutsch` controls; `/preview/setup-complete` rendered the empty first-source handoff in Spanish with `Elige tu primera fuente de infraestructura`, `Credenciales que debes guardar ahora`, `Agregar infraestructura`, and `Instalar Pulse Agent`; `/preview/setup-complete?scenario=vmware-api-backed` rendered the connected handoff in German with `Erstes ueberwachtes System verbunden` and `Infrastruktur oeffnen` while preserving `vCenter Prod`, `VMware vSphere`, `API`, and `Pulse Agent`.
|
||||
|
||||
## Residuals
|
||||
|
||||
- The broader localized-product-surfaces lane remains open. This slice does not translate website/acquisition copy, install and troubleshooting docs outside the first-session UI, Settings surfaces beyond Settings General, core monitoring tables, alerts/operator-action copy, mobile handoff copy, commercial trust surfaces, public checkout/account surfaces, or native mobile surfaces.
|
||||
- Locale QA remains local guardrail and browser proof for the migrated frontend-modern journey. Broader screenshot review, native speaker review, pseudo-locale coverage, extraction tooling, and future French/Brazilian Portuguese/Japanese/Simplified Chinese/Korean expansion remain separate governed work.
|
||||
- Machine-facing payloads, log text, config keys, API fields, commands, source resource names, URLs, generated credentials, and user-entered identifiers remain intentionally untranslated.
|
||||
@@ -6929,11 +6929,26 @@
|
||||
"proposed_resolution": "new-lane",
|
||||
"coverage_impact": 8,
|
||||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/release-control/v6/internal/records/localized-product-surfaces-first-session-monitoring-2026-06-14.md",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/release-control/v6/internal/records/localized-product-surfaces-foundation-2026-06-13.md",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/release-control/v6/internal/subsystems/agent-lifecycle.md",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/release-control/v6/internal/subsystems/cloud-paid.md",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/release-control/v6/internal/subsystems/frontend-primitives.md",
|
||||
@@ -6959,6 +6974,51 @@
|
||||
"path": "frontend-modern/src/components/Settings/settingsNavCatalog.ts",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPanel.guardrails.test.ts",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.localization.test.tsx",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/setupCompletionModel.ts",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/SetupWizard.tsx",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/StepIndicator.tsx",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/i18n/__tests__/i18n.test.ts",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/i18n/messages.ts",
|
||||
@@ -6968,6 +7028,11 @@
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/i18n/policy.ts",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "frontend-modern/src/pages/RuntimeHome.tsx",
|
||||
"kind": "file"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1127,6 +1127,12 @@ instead of lifecycle-local centered icon/text shells.
|
||||
may accept a preview-provided connected-resource override, but the live
|
||||
first-session runtime path must keep `/api/state` polling as the sole
|
||||
source of connected-system truth when no override is supplied.
|
||||
Localized setup-completion copy may route through
|
||||
`frontend-modern/src/i18n/messages.ts`, but the lifecycle contract remains
|
||||
unchanged: add-infrastructure and agent-install destinations, API token
|
||||
meaning, generated credential values, install/source product identifiers,
|
||||
and reported resource names must stay stable while only surrounding
|
||||
operator-facing guidance translates.
|
||||
7a. Keep lifecycle-neutral shared `internal/api/` changes from altering agent
|
||||
setup, registration, install, or profile payloads by accident. AI runtime
|
||||
or entitlement work that touches shared router or handler wiring must keep
|
||||
|
||||
@@ -924,6 +924,10 @@ acquisition destinations. The Docker / Podman runtime route remains `/docker`
|
||||
for search familiarity, but the shell may label the
|
||||
destination as Docker so it reads as the recognizable container runtime lens
|
||||
rather than an owning infrastructure platform.
|
||||
Localized runtime-home loading copy may route through the shared frontend i18n
|
||||
catalog, but this does not move route ownership or commercial posture into
|
||||
localization: authenticated `/`, hosted workspaces, public-demo suppression, and
|
||||
entitlement-read prohibitions remain governed by this cloud-paid boundary.
|
||||
|
||||
The Proxmox platform tab in the authenticated app shell is ordinary product
|
||||
navigation. It must stay separate from hosted acquisition, monitored-system
|
||||
|
||||
@@ -200,7 +200,12 @@ fields, config keys, log lines, error codes, hostnames, resource names, product
|
||||
identifiers, and vendor object names stay untranslated unless the owning
|
||||
runtime contract explicitly says otherwise. Migrated settings surfaces must
|
||||
render customer-facing copy through the catalog and shared presentation helpers
|
||||
rather than reintroducing panel-local English.
|
||||
rather than reintroducing panel-local English. Migrated first-session surfaces,
|
||||
including the Setup Wizard shell, welcome/security steps, setup completion
|
||||
handoff, and runtime-home loading handoff, follow the same catalog path; their
|
||||
guardrails must fail if the migrated journey returns to page-local English or
|
||||
translates commands, URLs, generated credentials, product/source identifiers, or
|
||||
reported resource names.
|
||||
|
||||
Alert thresholds consume the shared FilterBar primitive and route state, while
|
||||
the alerts subsystem owns the resource data and platform-specific threshold
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, createSignal, createEffect, createMemo, onCleanup, Show, For } from 'solid-js';
|
||||
import { t } from '@/i18n';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { apiFetchJSON } from '@/utils/apiClient';
|
||||
@@ -21,21 +22,6 @@ interface CompleteStepProps {
|
||||
connectedResourcesOverride?: readonly Resource[];
|
||||
}
|
||||
|
||||
const SOURCE_STRATEGY_OPTIONS = [
|
||||
{
|
||||
title: 'Platform API',
|
||||
description: 'Inventory and health from Proxmox, TrueNAS, VMware, PBS, or PMG.',
|
||||
},
|
||||
{
|
||||
title: 'Pulse Agent',
|
||||
description: 'Node-local telemetry for standalone hosts, services, Docker, and Kubernetes.',
|
||||
},
|
||||
{
|
||||
title: 'Use both',
|
||||
description: 'Combine platform inventory with Agent telemetry when full coverage matters.',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const ADD_INFRASTRUCTURE_PATH = buildInfrastructureOnboardingPath('pick');
|
||||
const AGENT_INSTALL_PATH = buildInfrastructureOnboardingPath('linux-host');
|
||||
const INFRASTRUCTURE_WORKSPACE_PATH = buildInfrastructureWorkspacePath();
|
||||
@@ -115,29 +101,14 @@ export const SetupCompletionPanel: Component<CompleteStepProps> = (props) => {
|
||||
const downloadCredentials = () => {
|
||||
const baseUrl = getPulseBaseUrl();
|
||||
const infrastructureUrl = `${baseUrl.replace(/\/$/, '')}${ADD_INFRASTRUCTURE_PATH}`;
|
||||
const content = `Pulse Credentials
|
||||
==================
|
||||
Generated: ${new Date().toISOString()}
|
||||
|
||||
Web Login:
|
||||
----------
|
||||
URL: ${baseUrl}
|
||||
Username: ${props.state.username}
|
||||
Password: ${props.state.password}
|
||||
|
||||
Admin API Token:
|
||||
----------------
|
||||
${props.state.apiToken}
|
||||
|
||||
Infrastructure:
|
||||
---------------
|
||||
${infrastructureUrl}
|
||||
|
||||
Use Add infrastructure to choose a platform API, Pulse Agent, or both
|
||||
for the first system Pulse should monitor.
|
||||
|
||||
Keep these credentials secure!
|
||||
`;
|
||||
const content = t('setup.completion.download.content', {
|
||||
generatedAt: new Date().toISOString(),
|
||||
baseUrl,
|
||||
username: props.state.username,
|
||||
password: props.state.password,
|
||||
apiToken: props.state.apiToken,
|
||||
infrastructureUrl,
|
||||
});
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
@@ -163,6 +134,20 @@ Keep these credentials secure!
|
||||
};
|
||||
|
||||
const completionViewModel = createMemo(() => buildSetupCompletionViewModel(connectedSystems()));
|
||||
const sourceStrategyOptions = createMemo(() => [
|
||||
{
|
||||
title: t('setup.completion.sourceOptions.platformApi.title'),
|
||||
description: t('setup.completion.sourceOptions.platformApi.description'),
|
||||
},
|
||||
{
|
||||
title: t('setup.completion.sourceOptions.agent.title'),
|
||||
description: t('setup.completion.sourceOptions.agent.description'),
|
||||
},
|
||||
{
|
||||
title: t('setup.completion.sourceOptions.both.title'),
|
||||
description: t('setup.completion.sourceOptions.both.description'),
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<div class="max-w-2xl mx-auto bg-surface border border-border overflow-hidden relative rounded-md p-6 sm:p-8 text-center text-base-content">
|
||||
@@ -246,14 +231,15 @@ Keep these credentials secure!
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-base-content font-semibold text-sm flex items-center gap-2 flex-wrap">
|
||||
Credentials you must save now
|
||||
{t('setup.completion.credentials.title')}
|
||||
<span class="text-[10px] text-amber-700 dark:text-amber-400 bg-amber-100 dark:bg-amber-900 border border-amber-200 dark:border-amber-800 px-2 py-0.5 rounded-full">
|
||||
Shown during setup
|
||||
{t('setup.completion.credentials.badge')}
|
||||
</span>
|
||||
</span>
|
||||
<p class="mt-1 text-xs text-muted max-w-xl">
|
||||
Save the admin login and API token before leaving this screen, then continue into{' '}
|
||||
{completionViewModel().credentialsContinuationText}
|
||||
{t('setup.completion.credentials.description', {
|
||||
destination: completionViewModel().credentialsContinuationText,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -276,14 +262,14 @@ Keep these credentials secure!
|
||||
<div class="px-4 pb-4 pt-0 sm:px-6 sm:pb-6 space-y-3 border-t border-border-subtle">
|
||||
<div class="bg-surface-hover dark:bg-black border rounded-md p-3 mt-4">
|
||||
<div class="text-[11px] font-medium text-muted mb-1 uppercase tracking-wider">
|
||||
Username
|
||||
{t('setup.completion.credentials.usernameLabel')}
|
||||
</div>
|
||||
<div class="text-base-content font-mono text-sm">{props.state.username}</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-surface-hover dark:bg-black border rounded-md p-3">
|
||||
<div class="text-[11px] font-medium text-muted mb-1 uppercase tracking-wider">
|
||||
Password
|
||||
{t('setup.completion.credentials.passwordLabel')}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<code class="text-base-content font-mono text-sm break-all">
|
||||
@@ -328,7 +314,7 @@ Keep these credentials secure!
|
||||
|
||||
<div class="bg-surface-hover dark:bg-black border border-border rounded-md p-3">
|
||||
<div class="text-[11px] font-medium text-muted mb-1 uppercase tracking-wider">
|
||||
Admin API Token
|
||||
{t('setup.completion.credentials.apiTokenLabel')}
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<code class="text-base-content font-mono text-xs break-all pr-4">
|
||||
@@ -383,14 +369,14 @@ Keep these credentials secure!
|
||||
d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||
/>
|
||||
</svg>
|
||||
Download credentials
|
||||
{t('setup.completion.action.downloadCredentials')}
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div
|
||||
aria-label="Setup next step"
|
||||
aria-label={t('setup.completion.nextStep.ariaLabel')}
|
||||
class="bg-surface rounded-md border border-border p-5 sm:p-6 text-left mb-6"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
@@ -413,11 +399,13 @@ Keep these credentials secure!
|
||||
</h3>
|
||||
</div>
|
||||
<div class="rounded-sm bg-blue-50 px-2 py-1 text-[10px] font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
|
||||
Recommended next step
|
||||
{t('setup.completion.nextStep.badge')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 rounded-md border border-border bg-surface-alt p-4">
|
||||
<div class="text-[11px] font-medium uppercase tracking-wider text-muted">Next step</div>
|
||||
<div class="text-[11px] font-medium uppercase tracking-wider text-muted">
|
||||
{t('setup.completion.nextStep.label')}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-base-content">
|
||||
{completionViewModel().nextStepSummary}
|
||||
</div>
|
||||
@@ -432,15 +420,15 @@ Keep these credentials secure!
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-3 text-sm font-semibold text-white transition-colors hover:bg-blue-700"
|
||||
>
|
||||
{completionViewModel().primaryAction === 'infrastructure'
|
||||
? 'Open Infrastructure'
|
||||
: 'Add infrastructure'}
|
||||
? t('setup.completion.action.openInfrastructure')
|
||||
: t('setup.completion.action.addInfrastructure')}
|
||||
</button>
|
||||
<Show when={completionViewModel().showAddInfrastructureAction}>
|
||||
<button
|
||||
onClick={handleOpenAddInfrastructure}
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md border border-border px-4 py-3 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
Add infrastructure
|
||||
{t('setup.completion.action.addInfrastructure')}
|
||||
</button>
|
||||
</Show>
|
||||
<Show when={completionViewModel().showAgentInstallAction}>
|
||||
@@ -448,16 +436,16 @@ Keep these credentials secure!
|
||||
onClick={handleOpenAgentInstall}
|
||||
class="inline-flex items-center justify-center gap-2 rounded-md border border-border px-4 py-3 text-sm font-medium text-base-content transition-colors hover:bg-surface-hover"
|
||||
>
|
||||
Install Pulse Agent
|
||||
{t('setup.completion.action.installAgent')}
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="mt-4 border-t border-border-subtle pt-3">
|
||||
<div class="text-[11px] font-medium uppercase tracking-wider text-muted">
|
||||
Source choices
|
||||
{t('setup.completion.sourceOptions.title')}
|
||||
</div>
|
||||
<ul class="mt-2 space-y-1.5 text-left">
|
||||
<For each={SOURCE_STRATEGY_OPTIONS}>
|
||||
<For each={sourceStrategyOptions()}>
|
||||
{(option) => (
|
||||
<li class="text-xs leading-snug">
|
||||
<span class="font-semibold text-base-content">{option.title}</span>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import { Component, createMemo, createSignal, Show } from 'solid-js';
|
||||
import { t } from '@/i18n';
|
||||
import { WelcomeStep } from './steps/WelcomeStep';
|
||||
import { SecurityStep } from './steps/SecurityStep';
|
||||
import { SetupCompletionPanel } from './SetupCompletionPanel';
|
||||
@@ -59,15 +60,27 @@ export const SetupWizard: Component<SetupWizardProps> = (props) => {
|
||||
setCurrentStep('completion');
|
||||
};
|
||||
|
||||
const stepLabels = ['Unlock server', 'Security', 'First source'];
|
||||
const stepLabels = createMemo(() => [
|
||||
t('setup.step.unlockServer'),
|
||||
t('setup.step.security'),
|
||||
t('setup.step.firstSource'),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div class="min-h-screen bg-base flex flex-col" role="main" aria-label="Pulse Setup Wizard">
|
||||
<div
|
||||
class="min-h-screen bg-base flex flex-col"
|
||||
role="main"
|
||||
aria-label={t('setup.wizard.ariaLabel')}
|
||||
>
|
||||
{/* Background decoration */}
|
||||
<div class="fixed inset-0 overflow-hidden pointer-events-none" aria-hidden="true"></div>
|
||||
|
||||
<div class="relative z-10 pt-8 px-4" role="navigation" aria-label="Setup progress">
|
||||
<StepIndicator steps={stepLabels} currentStep={currentStepIndex()} />
|
||||
<div
|
||||
class="relative z-10 pt-8 px-4"
|
||||
role="navigation"
|
||||
aria-label={t('setup.progress.ariaLabel')}
|
||||
>
|
||||
<StepIndicator steps={stepLabels()} currentStep={currentStepIndex()} />
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component } from 'solid-js';
|
||||
import { t } from '@/i18n';
|
||||
|
||||
interface StepIndicatorProps {
|
||||
steps: string[];
|
||||
@@ -11,9 +12,9 @@ export const StepIndicator: Component<StepIndicatorProps> = (props) => {
|
||||
{props.steps.map((step, index) => {
|
||||
const state =
|
||||
index < props.currentStep
|
||||
? ', completed'
|
||||
? t('setup.progress.state.completed')
|
||||
: index === props.currentStep
|
||||
? ', current'
|
||||
? t('setup.progress.state.current')
|
||||
: '';
|
||||
return (
|
||||
<div class="flex items-center" role="listitem">
|
||||
@@ -26,7 +27,11 @@ export const StepIndicator: Component<StepIndicatorProps> = (props) => {
|
||||
: 'bg-surface border border-border text-muted'
|
||||
}`}
|
||||
aria-current={index === props.currentStep ? 'step' : undefined}
|
||||
aria-label={`Step ${index + 1}: ${step}${state}`}
|
||||
aria-label={t('setup.progress.stepAriaLabel', {
|
||||
index: index + 1,
|
||||
step,
|
||||
state,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
class={`w-5 h-5 flex items-center justify-center rounded-full text-xs ${
|
||||
|
||||
+73
-30
@@ -1,6 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import setupCompletionPanelSource from '../SetupCompletionPanel.tsx?raw';
|
||||
import setupCompletionModelSource from '../setupCompletionModel.ts?raw';
|
||||
import runtimeHomeSource from '@/pages/RuntimeHome.tsx?raw';
|
||||
import securityStepSource from '../steps/SecurityStep.tsx?raw';
|
||||
import setupWizardSource from '../SetupWizard.tsx?raw';
|
||||
import stepIndicatorSource from '../StepIndicator.tsx?raw';
|
||||
import welcomeStepSource from '../steps/WelcomeStep.tsx?raw';
|
||||
|
||||
const migratedFirstSessionCopy = [
|
||||
'Opening workspace...',
|
||||
'Welcome to Pulse',
|
||||
'Unlock this Pulse server',
|
||||
'Create the admin account',
|
||||
'Choose the first source',
|
||||
'Unlock setup',
|
||||
'What this token does',
|
||||
'Paste your bootstrap token',
|
||||
'Verify bootstrap token',
|
||||
'Continue to Security',
|
||||
'Create Account & Continue',
|
||||
'On the next screen',
|
||||
'Choose your first infrastructure source',
|
||||
'First monitored system connected',
|
||||
'Credentials you must save now',
|
||||
'Shown during setup',
|
||||
'Save the admin login and API token before leaving this screen',
|
||||
'Recommended next step',
|
||||
'Source choices',
|
||||
'Inventory and health from Proxmox, TrueNAS, VMware, PBS, or PMG.',
|
||||
'Node-local telemetry for standalone hosts, services, Docker, and Kubernetes.',
|
||||
'Open Add infrastructure to choose a platform API, Pulse Agent, or both.',
|
||||
];
|
||||
|
||||
const migratedSources = [
|
||||
setupCompletionPanelSource,
|
||||
setupCompletionModelSource,
|
||||
runtimeHomeSource,
|
||||
securityStepSource,
|
||||
setupWizardSource,
|
||||
stepIndicatorSource,
|
||||
welcomeStepSource,
|
||||
];
|
||||
|
||||
describe('SetupCompletionPanel guardrails', () => {
|
||||
it('keeps setup completion aligned with the canonical add-infrastructure picker', () => {
|
||||
@@ -14,21 +54,17 @@ describe('SetupCompletionPanel guardrails', () => {
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'const INFRASTRUCTURE_WORKSPACE_PATH = buildInfrastructureWorkspacePath();',
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain('Add infrastructure');
|
||||
expect(setupCompletionPanelSource).toContain('Install Pulse Agent');
|
||||
expect(setupCompletionPanelSource).toContain('Credentials you must save now');
|
||||
expect(setupCompletionPanelSource).toContain('Shown during setup');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.action.addInfrastructure');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.action.installAgent');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.credentials.title');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.credentials.badge');
|
||||
expect(setupCompletionPanelSource).toContain('props.onComplete(ADD_INFRASTRUCTURE_PATH);');
|
||||
expect(setupCompletionPanelSource).toContain('props.onComplete(AGENT_INSTALL_PATH);');
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'props.onComplete(INFRASTRUCTURE_WORKSPACE_PATH);',
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'Use Add infrastructure to choose a platform API, Pulse Agent, or both',
|
||||
);
|
||||
expect(setupCompletionModelSource).toContain(
|
||||
'Open Add infrastructure to choose a platform API, Pulse Agent, or both.',
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.download.content');
|
||||
expect(setupCompletionModelSource).toContain('setup.completion.nextStep.summary.empty');
|
||||
expect(setupCompletionPanelSource).not.toContain('Use Add connection to connect');
|
||||
expect(setupCompletionPanelSource).not.toContain("from '@/stores/licenseCommercial';");
|
||||
expect(setupCompletionPanelSource).not.toContain('runStartProTrialAction');
|
||||
@@ -43,21 +79,18 @@ describe('SetupCompletionPanel guardrails', () => {
|
||||
});
|
||||
|
||||
it('describes setup completion through one compact source-choice next-step surface', () => {
|
||||
expect(setupCompletionPanelSource).toContain('SOURCE_STRATEGY_OPTIONS');
|
||||
expect(setupCompletionPanelSource).toContain('Source choices');
|
||||
expect(setupCompletionPanelSource).toContain('sourceStrategyOptions');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.sourceOptions.title');
|
||||
expect(setupCompletionPanelSource).toContain('<ul class="mt-2 space-y-1.5 text-left">');
|
||||
expect(setupCompletionPanelSource).toContain("title: 'Platform API'");
|
||||
expect(setupCompletionPanelSource).toContain("title: 'Pulse Agent'");
|
||||
expect(setupCompletionPanelSource).toContain("title: 'Use both'");
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'Inventory and health from Proxmox, TrueNAS, VMware, PBS, or PMG.',
|
||||
'setup.completion.sourceOptions.platformApi.title',
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.sourceOptions.agent.title');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.sourceOptions.both.title');
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'Node-local telemetry for standalone hosts, services, Docker, and Kubernetes.',
|
||||
);
|
||||
expect(setupCompletionModelSource).toContain(
|
||||
'Start with a platform API when a platform manages the estate.',
|
||||
'setup.completion.sourceOptions.platformApi.description',
|
||||
);
|
||||
expect(setupCompletionModelSource).toContain('setup.completion.nextStep.detail.empty');
|
||||
expect(setupCompletionPanelSource).not.toContain("title: 'What happens next'");
|
||||
expect(setupCompletionPanelSource).not.toContain("title: 'Open Add infrastructure'");
|
||||
expect(setupCompletionPanelSource).not.toContain(
|
||||
@@ -116,16 +149,12 @@ describe('SetupCompletionPanel guardrails', () => {
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'const [showCredentials, setShowCredentials] = createSignal(true);',
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
'Save the admin login and API token before leaving this screen',
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain('Recommended next step');
|
||||
expect(setupCompletionPanelSource).toContain('aria-label="Setup next step"');
|
||||
expect(setupCompletionPanelSource).toContain('Open Infrastructure');
|
||||
expect(setupCompletionModelSource).toContain("heroTitle: 'First monitored system connected'");
|
||||
expect(setupCompletionModelSource).toContain(
|
||||
"heroTitle: 'Choose your first infrastructure source'",
|
||||
);
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.credentials.description');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.nextStep.badge');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.nextStep.ariaLabel');
|
||||
expect(setupCompletionPanelSource).toContain('setup.completion.action.openInfrastructure');
|
||||
expect(setupCompletionModelSource).toContain('setup.completion.hero.connected.title');
|
||||
expect(setupCompletionModelSource).toContain('setup.completion.hero.empty.title');
|
||||
expect(setupCompletionPanelSource).toContain(
|
||||
"completionViewModel().primaryAction === 'infrastructure'",
|
||||
);
|
||||
@@ -146,4 +175,18 @@ describe('SetupCompletionPanel guardrails', () => {
|
||||
'then return to Add infrastructure when you want to connect the next API or Agent source.',
|
||||
);
|
||||
});
|
||||
|
||||
it('prevents migrated first-session monitoring copy from reverting to hardcoded English', () => {
|
||||
for (const source of migratedSources) {
|
||||
for (const copy of migratedFirstSessionCopy) {
|
||||
expect(source).not.toContain(copy);
|
||||
}
|
||||
}
|
||||
|
||||
expect(welcomeStepSource).toContain('setup.welcome.hero.title');
|
||||
expect(securityStepSource).toContain('setup.security.title');
|
||||
expect(setupWizardSource).toContain('setup.wizard.ariaLabel');
|
||||
expect(stepIndicatorSource).toContain('setup.progress.stepAriaLabel');
|
||||
expect(runtimeHomeSource).toContain('runtimeHome.openingWorkspace');
|
||||
});
|
||||
});
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
|
||||
import { DEFAULT_LOCALE, setActiveLocale } from '@/i18n';
|
||||
import RuntimeHome from '@/pages/RuntimeHome';
|
||||
import { SetupCompletionPanel } from '../SetupCompletionPanel';
|
||||
import { SecurityStep } from '../steps/SecurityStep';
|
||||
import { WelcomeStep } from '../steps/WelcomeStep';
|
||||
import type { WizardState } from '../SetupWizard';
|
||||
|
||||
const apiFetchJSONMock = vi.fn();
|
||||
|
||||
vi.mock('@/utils/apiClient', () => ({
|
||||
apiFetch: vi.fn(),
|
||||
apiFetchJSON: (...args: unknown[]) => apiFetchJSONMock(...args),
|
||||
setApiToken: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/clipboard', () => ({
|
||||
copyToClipboard: vi.fn().mockResolvedValue(true),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/toast', () => ({
|
||||
showError: vi.fn(),
|
||||
showSuccess: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/utils/logger', () => ({
|
||||
logger: {
|
||||
error: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
info: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const baseState: WizardState = {
|
||||
username: 'admin',
|
||||
password: 'password',
|
||||
apiToken: 'setup-token',
|
||||
};
|
||||
|
||||
describe('localized setup wizard journey', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
apiFetchJSONMock.mockResolvedValue({
|
||||
bootstrapTokenPath: '/etc/pulse/.bootstrap_token',
|
||||
isDocker: false,
|
||||
inContainer: false,
|
||||
lxcCtid: '',
|
||||
dockerContainerName: '',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
setActiveLocale(DEFAULT_LOCALE);
|
||||
});
|
||||
|
||||
it('renders the welcome step in Spanish while preserving product and API tokens', async () => {
|
||||
setActiveLocale('es');
|
||||
|
||||
render(() => (
|
||||
<WelcomeStep
|
||||
onNext={vi.fn()}
|
||||
bootstrapToken=""
|
||||
setBootstrapToken={vi.fn()}
|
||||
isUnlocked={false}
|
||||
setIsUnlocked={vi.fn()}
|
||||
/>
|
||||
));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(apiFetchJSONMock).toHaveBeenCalledWith('/api/security/status');
|
||||
});
|
||||
|
||||
expect(screen.getByText('Bienvenido a Pulse')).toBeInTheDocument();
|
||||
expect(screen.getByText('Desbloquear configuración')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Conecta una API de plataforma/)).toBeInTheDocument();
|
||||
expect(screen.getByText('sudo pulse bootstrap-token')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Verificar token de bootstrap →' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the security step in German', () => {
|
||||
setActiveLocale('de');
|
||||
|
||||
render(() => (
|
||||
<SecurityStep
|
||||
state={baseState}
|
||||
updateState={vi.fn()}
|
||||
bootstrapToken="bootstrap-token"
|
||||
onComplete={vi.fn()}
|
||||
onBack={vi.fn()}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('Admin-Konto erstellen')).toBeInTheDocument();
|
||||
expect(screen.getByText('Automatisch erzeugen')).toBeInTheDocument();
|
||||
expect(screen.getByText('Auf dem naechsten Bildschirm')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Konto erstellen und fortfahren →' }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the completion handoff in Spanish for the empty first-source state', () => {
|
||||
setActiveLocale('es');
|
||||
|
||||
render(() => (
|
||||
<SetupCompletionPanel
|
||||
state={baseState}
|
||||
onComplete={vi.fn()}
|
||||
connectedResourcesOverride={[]}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('Elige tu primera fuente de infraestructura')).toBeInTheDocument();
|
||||
expect(screen.getByText('Credenciales que debes guardar ahora')).toBeInTheDocument();
|
||||
expect(screen.getByText('Opciones de fuente')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Agregar infraestructura' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Instalar Pulse Agent' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the connected monitoring handoff in German without translating resource names', () => {
|
||||
setActiveLocale('de');
|
||||
|
||||
render(() => (
|
||||
<SetupCompletionPanel
|
||||
state={baseState}
|
||||
onComplete={vi.fn()}
|
||||
connectedResourcesOverride={[
|
||||
{
|
||||
id: 'agent-1',
|
||||
type: 'agent',
|
||||
name: 'Tower',
|
||||
displayName: 'Tower',
|
||||
platformId: 'tower',
|
||||
platformType: 'agent',
|
||||
sourceType: 'agent',
|
||||
status: 'online',
|
||||
lastSeen: 123,
|
||||
agent: { agentId: 'agent-1' },
|
||||
},
|
||||
]}
|
||||
/>
|
||||
));
|
||||
|
||||
expect(screen.getByText('Erstes ueberwachtes System verbunden')).toBeInTheDocument();
|
||||
expect(screen.getByText('Verbunden (1 System)')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Tower').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByRole('button', { name: 'Infrastruktur oeffnen' }).length).toBeGreaterThan(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the runtime home handoff in Spanish', () => {
|
||||
setActiveLocale('es');
|
||||
|
||||
render(() => <RuntimeHome />);
|
||||
|
||||
expect(screen.getByText('Abriendo el espacio de trabajo...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library';
|
||||
import { DEFAULT_LOCALE, setActiveLocale } from '@/i18n';
|
||||
import { SetupWizard } from '../SetupWizard';
|
||||
|
||||
vi.mock('../steps/WelcomeStep', () => ({
|
||||
@@ -39,6 +40,7 @@ describe('SetupWizard', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
setActiveLocale(DEFAULT_LOCALE);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
@@ -65,4 +67,14 @@ describe('SetupWizard', () => {
|
||||
|
||||
expect(onComplete).toHaveBeenCalledWith('/settings/infrastructure?add=linux-host');
|
||||
});
|
||||
|
||||
it('passes localized first-session step labels into the shared progress indicator', () => {
|
||||
setActiveLocale('de');
|
||||
|
||||
render(() => <SetupWizard onComplete={vi.fn()} />);
|
||||
|
||||
expect(
|
||||
screen.getByText('Step indicator 0:Server entsperren > Sicherheit > Erste Quelle'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Resource } from '@/types/resource';
|
||||
import { t } from '@/i18n';
|
||||
import {
|
||||
getActionableAgentIdFromResource,
|
||||
getPlatformDataRecord,
|
||||
@@ -47,6 +48,11 @@ const asRecord = (value: unknown): Record<string, unknown> | undefined =>
|
||||
const asString = (value: unknown): string | undefined =>
|
||||
typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
||||
|
||||
const getUnknownSetupSystemName = (): string => t('setup.completion.resource.unknownName');
|
||||
|
||||
const isUnknownSetupSystemName = (name: string): boolean =>
|
||||
name === 'Unknown' || name === getUnknownSetupSystemName();
|
||||
|
||||
const isSetupCompletionInfrastructureResource = (resource: Resource): boolean =>
|
||||
resource.type === 'agent' || resource.type === 'pbs' || resource.type === 'pmg';
|
||||
|
||||
@@ -104,7 +110,10 @@ const toConnectedSetupSystem = (resource: Resource): ConnectedSetupSystem | null
|
||||
|
||||
return {
|
||||
id: resource.platformId || getActionableAgentIdFromResource(resource) || resource.id,
|
||||
name: getPreferredInfrastructureDisplayName(resource) || resource.name || 'Unknown',
|
||||
name:
|
||||
getPreferredInfrastructureDisplayName(resource) ||
|
||||
resource.name ||
|
||||
getUnknownSetupSystemName(),
|
||||
typeLabel: getConnectedSetupSystemTypeLabel(resource),
|
||||
host: getConnectedSetupSystemHost(resource),
|
||||
connectionPath,
|
||||
@@ -134,7 +143,7 @@ export function buildSetupCompletionConnectedSystems(
|
||||
if (!existing.host && nextSystem.host) {
|
||||
existing.host = nextSystem.host;
|
||||
}
|
||||
if (existing.name === 'Unknown' && nextSystem.name !== 'Unknown') {
|
||||
if (isUnknownSetupSystemName(existing.name) && !isUnknownSetupSystemName(nextSystem.name)) {
|
||||
existing.name = nextSystem.name;
|
||||
}
|
||||
}
|
||||
@@ -148,23 +157,22 @@ const buildConnectedHeroDescription = (
|
||||
hasAgentConnectedSystems: boolean,
|
||||
hasApiConnectedSystems: boolean,
|
||||
): string => {
|
||||
const prefix =
|
||||
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system';
|
||||
|
||||
if (hasAgentConnectedSystems && hasApiConnectedSystems) {
|
||||
return `${prefix}, then return to Add infrastructure when you want another platform API or Agent source.`;
|
||||
return t('setup.completion.hero.connected.description.both');
|
||||
}
|
||||
if (hasApiConnectedSystems) {
|
||||
return `${prefix}, then return to Add infrastructure when you want another platform API or Pulse Agent source.`;
|
||||
return t('setup.completion.hero.connected.description.api');
|
||||
}
|
||||
return `${prefix}, then return to Add infrastructure when you want another Pulse Agent or platform API source.`;
|
||||
return t('setup.completion.hero.connected.description.agent');
|
||||
};
|
||||
|
||||
const buildCredentialsContinuationText = (
|
||||
_hasAgentConnectedSystems: boolean,
|
||||
_hasApiConnectedSystems: boolean,
|
||||
hasAgentConnectedSystems: boolean,
|
||||
hasApiConnectedSystems: boolean,
|
||||
): string => {
|
||||
return 'Infrastructure or Add infrastructure.';
|
||||
return hasAgentConnectedSystems || hasApiConnectedSystems
|
||||
? t('setup.completion.credentials.continuation.connected')
|
||||
: t('setup.completion.credentials.continuation.empty');
|
||||
};
|
||||
|
||||
const buildConnectedNextStepDetail = (
|
||||
@@ -172,12 +180,12 @@ const buildConnectedNextStepDetail = (
|
||||
hasApiConnectedSystems: boolean,
|
||||
): string => {
|
||||
if (hasAgentConnectedSystems && hasApiConnectedSystems) {
|
||||
return 'Add infrastructure stays available any time you want to expand from this first system with another API source, Agent source, or both.';
|
||||
return t('setup.completion.nextStep.detail.both');
|
||||
}
|
||||
if (hasApiConnectedSystems) {
|
||||
return 'Add infrastructure stays available for more API-backed systems or Pulse Agent telemetry when a system needs node-local coverage.';
|
||||
return t('setup.completion.nextStep.detail.api');
|
||||
}
|
||||
return 'Add infrastructure stays available for more Pulse Agent systems or platform API inventory when a platform manages the estate.';
|
||||
return t('setup.completion.nextStep.detail.agent');
|
||||
};
|
||||
|
||||
export function buildSetupCompletionViewModel(
|
||||
@@ -189,26 +197,26 @@ export function buildSetupCompletionViewModel(
|
||||
);
|
||||
const hasApiConnectedSystems = connectedSystems.some((system) => system.connectionPath === 'api');
|
||||
|
||||
const connectedSystemNoun = connectedSystems.length === 1 ? 'system' : 'systems';
|
||||
const nextStepSummary =
|
||||
connectedSystems.length === 1
|
||||
? 'Open Infrastructure to review your first connected system.'
|
||||
: 'Open Infrastructure to review your connected systems.';
|
||||
? t('setup.completion.nextStep.summary.connected.singular')
|
||||
: t('setup.completion.nextStep.summary.connected.plural');
|
||||
|
||||
if (!hasConnectedSystems) {
|
||||
return {
|
||||
connectedSummaryLabel: 'Connected (0 systems)',
|
||||
credentialsContinuationText: 'Add infrastructure.',
|
||||
connectedSummaryLabel: t('setup.completion.connectedSummary.plural', { count: 0 }),
|
||||
credentialsContinuationText: buildCredentialsContinuationText(
|
||||
hasAgentConnectedSystems,
|
||||
hasApiConnectedSystems,
|
||||
),
|
||||
hasConnectedSystems,
|
||||
hasAgentConnectedSystems,
|
||||
hasApiConnectedSystems,
|
||||
heroDescription:
|
||||
'Your admin account is ready. Next, choose how the first system should enter the unified infrastructure model: platform API inventory, Pulse Agent telemetry, or both.',
|
||||
heroTitle: 'Choose your first infrastructure source',
|
||||
nextStepDetail:
|
||||
'Start with a platform API when a platform manages the estate. Install Pulse Agent when the system itself should report node-local telemetry.',
|
||||
nextStepSummary: 'Open Add infrastructure to choose a platform API, Pulse Agent, or both.',
|
||||
nextStepTitle: 'Choose the first source strategy',
|
||||
heroDescription: t('setup.completion.hero.empty.description'),
|
||||
heroTitle: t('setup.completion.hero.empty.title'),
|
||||
nextStepDetail: t('setup.completion.nextStep.detail.empty'),
|
||||
nextStepSummary: t('setup.completion.nextStep.summary.empty'),
|
||||
nextStepTitle: t('setup.completion.nextStep.title.empty'),
|
||||
primaryAction: 'sources',
|
||||
showAddInfrastructureAction: false,
|
||||
showAgentInstallAction: true,
|
||||
@@ -216,7 +224,12 @@ export function buildSetupCompletionViewModel(
|
||||
}
|
||||
|
||||
return {
|
||||
connectedSummaryLabel: `Connected (${connectedSystems.length} ${connectedSystemNoun})`,
|
||||
connectedSummaryLabel: t(
|
||||
connectedSystems.length === 1
|
||||
? 'setup.completion.connectedSummary.singular'
|
||||
: 'setup.completion.connectedSummary.plural',
|
||||
{ count: connectedSystems.length },
|
||||
),
|
||||
credentialsContinuationText: buildCredentialsContinuationText(
|
||||
hasAgentConnectedSystems,
|
||||
hasApiConnectedSystems,
|
||||
@@ -228,10 +241,10 @@ export function buildSetupCompletionViewModel(
|
||||
hasAgentConnectedSystems,
|
||||
hasApiConnectedSystems,
|
||||
),
|
||||
heroTitle: 'First monitored system connected',
|
||||
heroTitle: t('setup.completion.hero.connected.title'),
|
||||
nextStepDetail: buildConnectedNextStepDetail(hasAgentConnectedSystems, hasApiConnectedSystems),
|
||||
nextStepSummary,
|
||||
nextStepTitle: 'Open Infrastructure',
|
||||
nextStepTitle: t('setup.completion.nextStep.title.connected'),
|
||||
primaryAction: 'infrastructure',
|
||||
showAddInfrastructureAction: hasConnectedSystems,
|
||||
showAgentInstallAction: false,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import { t } from '@/i18n';
|
||||
import { showError } from '@/utils/toast';
|
||||
import { setApiToken as setApiClientToken, apiFetchJSON } from '@/utils/apiClient';
|
||||
import { STORAGE_KEYS } from '@/utils/localStorage';
|
||||
@@ -13,8 +14,7 @@ interface SecurityStepProps {
|
||||
}
|
||||
|
||||
const GENERATED_PASSWORD_LENGTH = 20;
|
||||
const GENERATED_PASSWORD_CHARS =
|
||||
'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%';
|
||||
const GENERATED_PASSWORD_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%';
|
||||
|
||||
export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
const [username, setUsername] = createSignal(props.state.username || 'admin');
|
||||
@@ -50,15 +50,15 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
const handleSetup = async () => {
|
||||
if (useCustomPassword()) {
|
||||
if (!password()) {
|
||||
showError('Please enter a password');
|
||||
showError(t('setup.security.error.passwordRequired'));
|
||||
return;
|
||||
}
|
||||
if (password().length < 12) {
|
||||
showError('Password must be at least 12 characters');
|
||||
showError(t('setup.security.error.passwordTooShort'));
|
||||
return;
|
||||
}
|
||||
if (password() !== confirmPassword()) {
|
||||
showError('Passwords do not match');
|
||||
showError(t('setup.security.error.passwordMismatch'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
|
||||
props.onComplete();
|
||||
} catch (error) {
|
||||
showError(`Setup failed: ${error}`);
|
||||
showError(t('setup.security.error.setupFailed', { error: String(error) }));
|
||||
} finally {
|
||||
setIsSettingUp(false);
|
||||
}
|
||||
@@ -122,26 +122,28 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
<div class="max-w-lg mx-auto bg-surface border border-border overflow-hidden relative rounded-md">
|
||||
<div class="p-8 border-b border-border relative z-10 text-center">
|
||||
<h2 class="text-2xl font-bold tracking-tight text-base-content">
|
||||
Create your admin account
|
||||
{t('setup.security.title')}
|
||||
</h2>
|
||||
<p class="text-sm text-muted mt-2">
|
||||
The next step is to choose the first infrastructure source.
|
||||
</p>
|
||||
<p class="text-sm text-muted mt-2">{t('setup.security.description')}</p>
|
||||
</div>
|
||||
<div class="p-8 space-y-6 relative z-10">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-base-content mb-2">Username</label>
|
||||
<label class="block text-sm font-medium text-base-content mb-2">
|
||||
{t('setup.security.label.username')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username()}
|
||||
onInput={(e) => setUsername(e.currentTarget.value)}
|
||||
class="w-full px-5 py-3.5 bg-surface border border-border rounded-md text-base-content placeholder-slate-400 focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors font-mono"
|
||||
placeholder="admin"
|
||||
placeholder={t('setup.security.placeholder.username')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-base-content mb-3">Password</label>
|
||||
<label class="block text-sm font-medium text-base-content mb-3">
|
||||
{t('setup.security.label.password')}
|
||||
</label>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
|
||||
<button
|
||||
type="button"
|
||||
@@ -152,7 +154,7 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
: 'bg-surface border-border text-muted hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
Auto-generate
|
||||
{t('setup.security.passwordMode.autoGenerate')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -163,7 +165,7 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
: 'bg-surface border-border text-muted hover:bg-surface-hover'
|
||||
}`}
|
||||
>
|
||||
Custom password
|
||||
{t('setup.security.customPassword')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -175,14 +177,16 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
value={password()}
|
||||
onInput={(e) => setPassword(e.currentTarget.value)}
|
||||
class="w-full px-5 py-3.5 pr-20 bg-surface border border-border rounded-md text-base-content placeholder-slate-400 focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors font-mono"
|
||||
placeholder="Password (min 12 characters)"
|
||||
placeholder={t('setup.security.placeholder.password')}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword())}
|
||||
class="absolute right-3 top-1/2 -translate-y-1/2 text-xs font-medium text-muted hover:text-base-content transition-colors px-2 py-1"
|
||||
>
|
||||
{showPassword() ? 'Hide' : 'Show'}
|
||||
{showPassword()
|
||||
? t('setup.security.showPassword.hide')
|
||||
: t('setup.security.showPassword.show')}
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
@@ -190,28 +194,26 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
value={confirmPassword()}
|
||||
onInput={(e) => setConfirmPassword(e.currentTarget.value)}
|
||||
class="w-full px-5 py-3.5 bg-surface border border-border rounded-md text-base-content placeholder-slate-400 focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors font-mono"
|
||||
placeholder="Confirm password"
|
||||
placeholder={t('setup.security.label.confirmPassword')}
|
||||
/>
|
||||
<p class="text-xs text-muted">Minimum 12 characters.</p>
|
||||
<p class="text-xs text-muted">{t('setup.security.minimumPasswordHelp')}</p>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!useCustomPassword()}>
|
||||
<p class="text-sm text-muted">
|
||||
A secure 20-character password will be generated and shown on the next screen.
|
||||
</p>
|
||||
<p class="text-sm text-muted">{t('setup.security.generatedPasswordHelp')}</p>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="bg-base rounded-md p-4 border border-border text-left">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-muted mb-2">
|
||||
On the next screen
|
||||
{t('setup.security.nextScreen.title')}
|
||||
</div>
|
||||
<ul class="text-sm text-muted space-y-1">
|
||||
<li>• Your username and password</li>
|
||||
<li>• An admin API token for automation</li>
|
||||
<li>• {t('setup.security.nextScreen.itemCredentials')}</li>
|
||||
<li>• {t('setup.security.nextScreen.itemApiToken')}</li>
|
||||
</ul>
|
||||
<p class="mt-2 text-xs text-muted">Save them before continuing, they are shown once.</p>
|
||||
<p class="mt-2 text-xs text-muted">{t('setup.security.nextScreen.saveOnce')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Actions */}
|
||||
@@ -220,7 +222,7 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
onClick={props.onBack}
|
||||
class="px-6 py-3.5 bg-surface border border-border hover:bg-surface-hover text-base-content font-medium rounded-md transition-colors"
|
||||
>
|
||||
← Back
|
||||
<span>←</span> {t('setup.security.action.back')}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSetup}
|
||||
@@ -248,10 +250,12 @@ export const SecurityStep: Component<SecurityStepProps> = (props) => {
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Setting up...
|
||||
{t('setup.security.action.settingUp')}
|
||||
</>
|
||||
) : (
|
||||
'Create Account & Continue →'
|
||||
<>
|
||||
{t('setup.security.action.createAccount')} <span>→</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Component, createMemo, createSignal, onMount, Show } from 'solid-js';
|
||||
import { t } from '@/i18n';
|
||||
import { showError, showSuccess } from '@/utils/toast';
|
||||
import { apiFetch, apiFetchJSON } from '@/utils/apiClient';
|
||||
import { logger } from '@/utils/logger';
|
||||
@@ -26,9 +27,9 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
if (copied) {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
showSuccess('Command copied to clipboard');
|
||||
showSuccess(t('setup.welcome.success.commandCopied'));
|
||||
} else {
|
||||
showError('Failed to copy command');
|
||||
showError(t('setup.welcome.error.copyCommandFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -76,17 +77,16 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const snapshotPasteHelp =
|
||||
'That looks like the encrypted .bootstrap_token file contents, not the raw setup token. Run the command above and paste the token string it prints.';
|
||||
const snapshotPasteHelp = () => t('setup.welcome.error.snapshotPaste');
|
||||
|
||||
const handleUnlock = async () => {
|
||||
const trimmedToken = props.bootstrapToken.trim();
|
||||
if (!trimmedToken) {
|
||||
showError('Please enter the bootstrap token');
|
||||
showError(t('setup.welcome.error.missingBootstrapToken'));
|
||||
return;
|
||||
}
|
||||
if (looksLikeBootstrapTokenSnapshot(trimmedToken)) {
|
||||
showError(snapshotPasteHelp);
|
||||
showError(snapshotPasteHelp());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -103,13 +103,13 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Invalid bootstrap token');
|
||||
throw new Error(t('setup.welcome.error.invalidBootstrapTokenResponse'));
|
||||
}
|
||||
|
||||
props.setIsUnlocked(true);
|
||||
props.onNext();
|
||||
} catch (_error) {
|
||||
showError('Invalid bootstrap token. Please check and try again.');
|
||||
showError(t('setup.welcome.error.invalidBootstrapToken'));
|
||||
} finally {
|
||||
setIsValidating(false);
|
||||
}
|
||||
@@ -130,37 +130,39 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
|
||||
const deploymentLabel = createMemo(() => {
|
||||
if (isDocker()) {
|
||||
return 'Docker deployment';
|
||||
return t('setup.welcome.deploymentLabel.docker');
|
||||
}
|
||||
if (inContainer() && lxcCtid()) {
|
||||
return 'LXC container';
|
||||
return t('setup.welcome.deploymentLabel.lxc');
|
||||
}
|
||||
if (inContainer()) {
|
||||
return 'Containerized deployment';
|
||||
return t('setup.welcome.deploymentLabel.containerized');
|
||||
}
|
||||
return 'Direct host install';
|
||||
return t('setup.welcome.deploymentLabel.direct');
|
||||
});
|
||||
|
||||
const deploymentHint = createMemo(() => {
|
||||
if (isDocker()) {
|
||||
return dockerContainerName()
|
||||
? `Pulse appears to be running in Docker as container "${dockerContainerName()}". Run the command on the Docker host to print the one-time setup token from that container.`
|
||||
: 'Pulse appears to be running in Docker. Run the command on the Docker host and replace <pulse-container> with the running Pulse container name.';
|
||||
? t('setup.welcome.deploymentHint.dockerNamed', {
|
||||
containerName: dockerContainerName(),
|
||||
})
|
||||
: t('setup.welcome.deploymentHint.dockerUnnamed');
|
||||
}
|
||||
if (inContainer() && lxcCtid()) {
|
||||
return `Pulse appears to be running in LXC container ${lxcCtid()}. Run the command on the Proxmox host to execute into that container and print the one-time setup token.`;
|
||||
return t('setup.welcome.deploymentHint.lxc', { ctid: lxcCtid() });
|
||||
}
|
||||
if (inContainer()) {
|
||||
return 'Pulse appears to be running in a containerized environment. Run the command from the host that manages the container so you can print the one-time setup token.';
|
||||
return t('setup.welcome.deploymentHint.containerized');
|
||||
}
|
||||
return 'Run the command directly in a shell on the Pulse server to print the one-time setup token.';
|
||||
return t('setup.welcome.deploymentHint.direct');
|
||||
});
|
||||
|
||||
const unlockHelp = createMemo(() => {
|
||||
if (isDocker()) {
|
||||
return 'This one-time bootstrap token only unlocks first-run setup. Run the command above and paste the token string it prints. After verification, you will create the admin account and Pulse will generate the long-lived API token separately.';
|
||||
return t('setup.welcome.tokenHelp.docker');
|
||||
}
|
||||
return 'This one-time bootstrap token only unlocks first-run setup on this Pulse server. Run the command above and paste the token string it prints. It is not your admin password and it is not the API token you will use after setup.';
|
||||
return t('setup.welcome.tokenHelp.host');
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -172,22 +174,25 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
class="w-20 h-20 rounded-md mb-6 mx-auto dark:shadow-none"
|
||||
/>
|
||||
<h1 class="text-3xl sm:text-4xl font-bold tracking-tight text-base-content mb-3">
|
||||
Welcome to Pulse
|
||||
{t('setup.welcome.hero.title')}
|
||||
</h1>
|
||||
<p class="text-base text-muted max-w-xl mx-auto">
|
||||
Three steps:{' '}
|
||||
<span class="text-base-content font-medium">Unlock this Pulse server</span>,{' '}
|
||||
<span class="text-base-content font-medium">Create the admin account</span>, then{' '}
|
||||
<span class="text-base-content font-medium">Choose the first source</span>.
|
||||
{t('setup.welcome.hero.stepsIntro')}{' '}
|
||||
<span class="text-base-content font-medium">{t('setup.welcome.hero.step.unlock')}</span>,{' '}
|
||||
<span class="text-base-content font-medium">{t('setup.welcome.hero.step.admin')}</span>,{' '}
|
||||
{t('setup.welcome.hero.stepsThen')}{' '}
|
||||
<span class="text-base-content font-medium">{t('setup.welcome.hero.step.source')}</span>.
|
||||
</p>
|
||||
<p class="text-sm text-muted max-w-xl mx-auto mt-2">
|
||||
<span>Connect a platform API, install Pulse Agent, or use both for full coverage.</span>
|
||||
<span>{t('setup.welcome.hero.coverage')}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Show when={!props.isUnlocked}>
|
||||
<div class="p-6 sm:p-8 max-w-2xl mx-auto bg-surface border border-border rounded-md text-left">
|
||||
<h3 class="text-lg font-semibold text-base-content mb-2 tracking-tight">Unlock setup</h3>
|
||||
<h3 class="text-lg font-semibold text-base-content mb-2 tracking-tight">
|
||||
{t('setup.welcome.unlockTitle')}
|
||||
</h3>
|
||||
<p class="text-sm text-muted mb-5">{deploymentHint()}</p>
|
||||
|
||||
<div class="mb-5">
|
||||
@@ -199,7 +204,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
<button
|
||||
onClick={copyCommand}
|
||||
class="ml-4 flex-shrink-0 p-2 rounded-md bg-surface hover:bg-slate-700 text-slate-300 hover:text-white transition-colors focus:outline-none focus:ring-0"
|
||||
title="Copy command"
|
||||
title={t('setup.welcome.copyCommandTitle')}
|
||||
>
|
||||
<Show when={copied()} fallback={<Copy class="w-4 h-4" />}>
|
||||
<Check class="w-4 h-4 text-emerald-400" />
|
||||
@@ -215,76 +220,75 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
|
||||
<div class="mb-5 text-left">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-muted">
|
||||
What this token does
|
||||
{t('setup.welcome.tokenHelp.title')}
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-muted">{unlockHelp()}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<input
|
||||
type="text"
|
||||
value={props.bootstrapToken}
|
||||
onInput={(e) => {
|
||||
const val = e.currentTarget.value;
|
||||
props.setBootstrapToken(val);
|
||||
// Premium UX: Auto-submit if we detect a pasted token (length heuristic)
|
||||
if (val.length > 20) {
|
||||
setTimeout(() => {
|
||||
if (props.bootstrapToken === val && !isValidating()) {
|
||||
handleUnlock();
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => e.key === 'Enter' && void handleUnlock()}
|
||||
class="w-full px-5 py-3.5 bg-surface border border-border rounded-md text-base-content placeholder-slate-400 focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors font-mono"
|
||||
placeholder="Paste your bootstrap token"
|
||||
autofocus
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={props.bootstrapToken}
|
||||
onInput={(e) => {
|
||||
const val = e.currentTarget.value;
|
||||
props.setBootstrapToken(val);
|
||||
// Premium UX: Auto-submit if we detect a pasted token (length heuristic)
|
||||
if (val.length > 20) {
|
||||
setTimeout(() => {
|
||||
if (props.bootstrapToken === val && !isValidating()) {
|
||||
handleUnlock();
|
||||
}
|
||||
}, 400);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => e.key === 'Enter' && void handleUnlock()}
|
||||
class="w-full px-5 py-3.5 bg-surface border border-border rounded-md text-base-content placeholder-slate-400 focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors font-mono"
|
||||
placeholder={t('setup.welcome.placeholder.bootstrapToken')}
|
||||
autofocus
|
||||
/>
|
||||
|
||||
<p class="text-xs text-muted">
|
||||
After Pulse verifies this token, the next step is creating the admin account for
|
||||
this server.
|
||||
<p class="text-xs text-muted">{t('setup.welcome.tokenHelp.afterVerify')}</p>
|
||||
<Show when={looksLikeBootstrapTokenSnapshot(props.bootstrapToken)}>
|
||||
<p class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100">
|
||||
{snapshotPasteHelp()}
|
||||
</p>
|
||||
<Show when={looksLikeBootstrapTokenSnapshot(props.bootstrapToken)}>
|
||||
<p class="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100">
|
||||
{snapshotPasteHelp}
|
||||
</p>
|
||||
</Show>
|
||||
</Show>
|
||||
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={isValidating() || !props.bootstrapToken.trim()}
|
||||
class="w-full py-3.5 px-6 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:bg-surface-alt disabled:text-muted disabled:cursor-not-allowed text-white font-medium rounded-md transition-colors flex justify-center items-center gap-2 duration-200"
|
||||
>
|
||||
{isValidating() ? (
|
||||
<>
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-2 h-5 w-5 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
Verifying bootstrap token...
|
||||
</>
|
||||
) : (
|
||||
'Verify bootstrap token →'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleUnlock}
|
||||
disabled={isValidating() || !props.bootstrapToken.trim()}
|
||||
class="w-full py-3.5 px-6 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:bg-surface-alt disabled:text-muted disabled:cursor-not-allowed text-white font-medium rounded-md transition-colors flex justify-center items-center gap-2 duration-200"
|
||||
>
|
||||
{isValidating() ? (
|
||||
<>
|
||||
<svg
|
||||
class="animate-spin -ml-1 mr-2 h-5 w-5 text-white"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
fill="currentColor"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
></path>
|
||||
</svg>
|
||||
{t('setup.welcome.action.verifyingToken')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{t('setup.welcome.action.verifyToken')} <span>→</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -294,7 +298,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
|
||||
onClick={props.onNext}
|
||||
class="py-4 px-10 bg-blue-600 hover:bg-blue-700 text-white text-lg font-medium rounded-md transition-colors duration-200"
|
||||
>
|
||||
Continue to Security →
|
||||
{t('setup.welcome.action.continueSecurity')} <span>→</span>
|
||||
</button>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import {
|
||||
DEFAULT_LOCALE,
|
||||
FIRST_SESSION_MONITORING_ALLOWED_IDENTICAL_TRANSLATIONS,
|
||||
FIRST_SESSION_MONITORING_NON_TRANSLATABLE_TOKENS,
|
||||
FIRST_LOCALIZATION_LOCALES,
|
||||
I18N_MESSAGES,
|
||||
LOCALIZATION_FOUNDATION,
|
||||
LOCALIZED_FIRST_SESSION_MONITORING_JOURNEY_KEYS,
|
||||
LOCALIZED_SETTINGS_GENERAL_JOURNEY_KEYS,
|
||||
NEVER_TRANSLATE_COPY_RULES,
|
||||
NEXT_LOCALIZATION_LOCALES,
|
||||
@@ -128,6 +131,25 @@ describe('i18n foundation', () => {
|
||||
'"Update"',
|
||||
]),
|
||||
);
|
||||
expect(FIRST_SESSION_MONITORING_NON_TRANSLATABLE_TOKENS).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Pulse',
|
||||
'Pulse Agent',
|
||||
'API',
|
||||
'URL',
|
||||
'Docker',
|
||||
'Kubernetes',
|
||||
'Proxmox',
|
||||
'TrueNAS',
|
||||
'VMware',
|
||||
'PBS',
|
||||
'PMG',
|
||||
'.bootstrap_token',
|
||||
'sudo pulse bootstrap-token',
|
||||
'docker exec',
|
||||
'pct exec',
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('requires explicit first-wave translations for the migrated settings general journey', () => {
|
||||
@@ -162,4 +184,44 @@ describe('i18n foundation', () => {
|
||||
).toContain('PVE_POLLING_INTERVAL');
|
||||
}
|
||||
});
|
||||
|
||||
it('requires explicit first-wave translations for the migrated first-session monitoring journey', () => {
|
||||
for (const locale of FIRST_LOCALIZATION_LOCALES) {
|
||||
const allowedIdenticalKeys: ReadonlySet<I18nMessageKey> = new Set([
|
||||
...((SETTINGS_GENERAL_ALLOWED_IDENTICAL_TRANSLATIONS[locale] ??
|
||||
[]) as readonly I18nMessageKey[]),
|
||||
...((FIRST_SESSION_MONITORING_ALLOWED_IDENTICAL_TRANSLATIONS[locale] ??
|
||||
[]) as readonly I18nMessageKey[]),
|
||||
]);
|
||||
for (const key of LOCALIZED_FIRST_SESSION_MONITORING_JOURNEY_KEYS) {
|
||||
expect(I18N_MESSAGES[locale][key], `${locale}:${key}`).toBeTruthy();
|
||||
if (!allowedIdenticalKeys.has(key)) {
|
||||
expect(I18N_MESSAGES[locale][key], `${locale}:${key}`).not.toBe(I18N_MESSAGES.en[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps machine-facing identifiers unchanged in first-session monitoring catalog copy', () => {
|
||||
for (const locale of FIRST_LOCALIZATION_LOCALES) {
|
||||
expect(I18N_MESSAGES[locale]['setup.welcome.deploymentHint.dockerUnnamed']).toContain(
|
||||
'<pulse-container>',
|
||||
);
|
||||
expect(I18N_MESSAGES[locale]['setup.welcome.deploymentHint.lxc']).toContain('Proxmox');
|
||||
expect(I18N_MESSAGES[locale]['setup.welcome.error.snapshotPaste']).toContain(
|
||||
'.bootstrap_token',
|
||||
);
|
||||
expect(I18N_MESSAGES[locale]['setup.completion.download.content']).toContain('URL:');
|
||||
expect(
|
||||
I18N_MESSAGES[locale]['setup.completion.sourceOptions.platformApi.description'],
|
||||
).toContain('Proxmox');
|
||||
expect(
|
||||
I18N_MESSAGES[locale]['setup.completion.sourceOptions.platformApi.description'],
|
||||
).toContain('TrueNAS');
|
||||
expect(I18N_MESSAGES[locale]['setup.completion.sourceOptions.agent.description']).toContain(
|
||||
'Docker',
|
||||
);
|
||||
expect(I18N_MESSAGES[locale]['setup.security.nextScreen.itemApiToken']).toContain('API');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,10 @@ export {
|
||||
type SupportedLocale,
|
||||
} from './locales';
|
||||
export {
|
||||
FIRST_SESSION_MONITORING_ALLOWED_IDENTICAL_TRANSLATIONS,
|
||||
FIRST_SESSION_MONITORING_NON_TRANSLATABLE_TOKENS,
|
||||
LOCALIZATION_FOUNDATION,
|
||||
LOCALIZED_FIRST_SESSION_MONITORING_JOURNEY_KEYS,
|
||||
LOCALIZED_SETTINGS_GENERAL_JOURNEY_KEYS,
|
||||
NEVER_TRANSLATE_COPY_RULES,
|
||||
SETTINGS_GENERAL_ALLOWED_IDENTICAL_TRANSLATIONS,
|
||||
|
||||
@@ -1,6 +1,140 @@
|
||||
import type { SupportedLocale } from './locales';
|
||||
|
||||
export const EN_MESSAGES = {
|
||||
'runtimeHome.openingWorkspace': 'Opening workspace...',
|
||||
'setup.completion.action.addInfrastructure': 'Add infrastructure',
|
||||
'setup.completion.action.downloadCredentials': 'Download credentials',
|
||||
'setup.completion.action.installAgent': 'Install Pulse Agent',
|
||||
'setup.completion.action.openInfrastructure': 'Open Infrastructure',
|
||||
'setup.completion.connectedSummary.singular': 'Connected ({count} system)',
|
||||
'setup.completion.connectedSummary.plural': 'Connected ({count} systems)',
|
||||
'setup.completion.credentials.apiTokenLabel': 'Admin API Token',
|
||||
'setup.completion.credentials.badge': 'Shown during setup',
|
||||
'setup.completion.credentials.continuation.empty': 'Add infrastructure.',
|
||||
'setup.completion.credentials.continuation.connected': 'Infrastructure or Add infrastructure.',
|
||||
'setup.completion.credentials.description':
|
||||
'Save the admin login and API token before leaving this screen, then continue into {destination}',
|
||||
'setup.completion.credentials.passwordLabel': 'Password',
|
||||
'setup.completion.credentials.title': 'Credentials you must save now',
|
||||
'setup.completion.credentials.usernameLabel': 'Username',
|
||||
'setup.completion.download.content':
|
||||
'Pulse Credentials\n==================\nGenerated: {generatedAt}\n\nWeb Login:\n----------\nURL: {baseUrl}\nUsername: {username}\nPassword: {password}\n\nAdmin API Token:\n----------------\n{apiToken}\n\nInfrastructure:\n---------------\n{infrastructureUrl}\n\nUse Add infrastructure to choose a platform API, Pulse Agent, or both\nfor the first system Pulse should monitor.\n\nKeep these credentials secure!\n',
|
||||
'setup.completion.hero.connected.description.agent':
|
||||
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system, then return to Add infrastructure when you want another Pulse Agent or platform API source.',
|
||||
'setup.completion.hero.connected.description.api':
|
||||
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system, then return to Add infrastructure when you want another platform API or Pulse Agent source.',
|
||||
'setup.completion.hero.connected.description.both':
|
||||
'Your admin account is ready and Pulse is already receiving telemetry. Open Infrastructure to inspect the first system, then return to Add infrastructure when you want another platform API or Agent source.',
|
||||
'setup.completion.hero.connected.title': 'First monitored system connected',
|
||||
'setup.completion.hero.empty.description':
|
||||
'Your admin account is ready. Next, choose how the first system should enter the unified infrastructure model: platform API inventory, Pulse Agent telemetry, or both.',
|
||||
'setup.completion.hero.empty.title': 'Choose your first infrastructure source',
|
||||
'setup.completion.nextStep.ariaLabel': 'Setup next step',
|
||||
'setup.completion.nextStep.badge': 'Recommended next step',
|
||||
'setup.completion.nextStep.detail.agent':
|
||||
'Add infrastructure stays available for more Pulse Agent systems or platform API inventory when a platform manages the estate.',
|
||||
'setup.completion.nextStep.detail.api':
|
||||
'Add infrastructure stays available for more API-backed systems or Pulse Agent telemetry when a system needs node-local coverage.',
|
||||
'setup.completion.nextStep.detail.both':
|
||||
'Add infrastructure stays available any time you want to expand from this first system with another API source, Agent source, or both.',
|
||||
'setup.completion.nextStep.detail.empty':
|
||||
'Start with a platform API when a platform manages the estate. Install Pulse Agent when the system itself should report node-local telemetry.',
|
||||
'setup.completion.nextStep.label': 'Next step',
|
||||
'setup.completion.nextStep.summary.connected.singular':
|
||||
'Open Infrastructure to review your first connected system.',
|
||||
'setup.completion.nextStep.summary.connected.plural':
|
||||
'Open Infrastructure to review your connected systems.',
|
||||
'setup.completion.nextStep.summary.empty':
|
||||
'Open Add infrastructure to choose a platform API, Pulse Agent, or both.',
|
||||
'setup.completion.nextStep.title.connected': 'Open Infrastructure',
|
||||
'setup.completion.nextStep.title.empty': 'Choose the first source strategy',
|
||||
'setup.completion.resource.unknownName': 'Unknown',
|
||||
'setup.completion.sourceOptions.agent.description':
|
||||
'Node-local telemetry for standalone hosts, services, Docker, and Kubernetes.',
|
||||
'setup.completion.sourceOptions.agent.title': 'Pulse Agent',
|
||||
'setup.completion.sourceOptions.both.description':
|
||||
'Combine platform inventory with Agent telemetry when full coverage matters.',
|
||||
'setup.completion.sourceOptions.both.title': 'Use both',
|
||||
'setup.completion.sourceOptions.platformApi.description':
|
||||
'Inventory and health from Proxmox, TrueNAS, VMware, PBS, or PMG.',
|
||||
'setup.completion.sourceOptions.platformApi.title': 'Platform API',
|
||||
'setup.completion.sourceOptions.title': 'Source choices',
|
||||
'setup.progress.ariaLabel': 'Setup progress',
|
||||
'setup.progress.state.completed': ', completed',
|
||||
'setup.progress.state.current': ', current',
|
||||
'setup.progress.stepAriaLabel': 'Step {index}: {step}{state}',
|
||||
'setup.security.action.back': 'Back',
|
||||
'setup.security.action.createAccount': 'Create Account & Continue',
|
||||
'setup.security.action.settingUp': 'Setting up...',
|
||||
'setup.security.customPassword': 'Custom password',
|
||||
'setup.security.description': 'The next step is to choose the first infrastructure source.',
|
||||
'setup.security.error.passwordMismatch': 'Passwords do not match',
|
||||
'setup.security.error.passwordRequired': 'Please enter a password',
|
||||
'setup.security.error.passwordTooShort': 'Password must be at least 12 characters',
|
||||
'setup.security.error.setupFailed': 'Setup failed: {error}',
|
||||
'setup.security.generatedPasswordHelp':
|
||||
'A secure 20-character password will be generated and shown on the next screen.',
|
||||
'setup.security.label.confirmPassword': 'Confirm password',
|
||||
'setup.security.label.password': 'Password',
|
||||
'setup.security.label.username': 'Username',
|
||||
'setup.security.minimumPasswordHelp': 'Minimum 12 characters.',
|
||||
'setup.security.nextScreen.itemApiToken': 'An admin API token for automation',
|
||||
'setup.security.nextScreen.itemCredentials': 'Your username and password',
|
||||
'setup.security.nextScreen.saveOnce': 'Save them before continuing, they are shown once.',
|
||||
'setup.security.nextScreen.title': 'On the next screen',
|
||||
'setup.security.passwordMode.autoGenerate': 'Auto-generate',
|
||||
'setup.security.placeholder.password': 'Password (min 12 characters)',
|
||||
'setup.security.placeholder.username': 'admin',
|
||||
'setup.security.showPassword.hide': 'Hide',
|
||||
'setup.security.showPassword.show': 'Show',
|
||||
'setup.security.title': 'Create your admin account',
|
||||
'setup.step.firstSource': 'First source',
|
||||
'setup.step.security': 'Security',
|
||||
'setup.step.unlockServer': 'Unlock server',
|
||||
'setup.wizard.ariaLabel': 'Pulse Setup Wizard',
|
||||
'setup.welcome.action.continueSecurity': 'Continue to Security',
|
||||
'setup.welcome.action.verifyToken': 'Verify bootstrap token',
|
||||
'setup.welcome.action.verifyingToken': 'Verifying bootstrap token...',
|
||||
'setup.welcome.copyCommandTitle': 'Copy command',
|
||||
'setup.welcome.deploymentHint.containerized':
|
||||
'Pulse appears to be running in a containerized environment. Run the command from the host that manages the container so you can print the one-time setup token.',
|
||||
'setup.welcome.deploymentHint.direct':
|
||||
'Run the command directly in a shell on the Pulse server to print the one-time setup token.',
|
||||
'setup.welcome.deploymentHint.dockerNamed':
|
||||
'Pulse appears to be running in Docker as container "{containerName}". Run the command on the Docker host to print the one-time setup token from that container.',
|
||||
'setup.welcome.deploymentHint.dockerUnnamed':
|
||||
'Pulse appears to be running in Docker. Run the command on the Docker host and replace <pulse-container> with the running Pulse container name.',
|
||||
'setup.welcome.deploymentHint.lxc':
|
||||
'Pulse appears to be running in LXC container {ctid}. Run the command on the Proxmox host to execute into that container and print the one-time setup token.',
|
||||
'setup.welcome.deploymentLabel.containerized': 'Containerized deployment',
|
||||
'setup.welcome.deploymentLabel.direct': 'Direct host install',
|
||||
'setup.welcome.deploymentLabel.docker': 'Docker deployment',
|
||||
'setup.welcome.deploymentLabel.lxc': 'LXC container',
|
||||
'setup.welcome.error.copyCommandFailed': 'Failed to copy command',
|
||||
'setup.welcome.error.invalidBootstrapToken':
|
||||
'Invalid bootstrap token. Please check and try again.',
|
||||
'setup.welcome.error.invalidBootstrapTokenResponse': 'Invalid bootstrap token',
|
||||
'setup.welcome.error.missingBootstrapToken': 'Please enter the bootstrap token',
|
||||
'setup.welcome.error.snapshotPaste':
|
||||
'That looks like the encrypted .bootstrap_token file contents, not the raw setup token. Run the command above and paste the token string it prints.',
|
||||
'setup.welcome.hero.coverage':
|
||||
'Connect a platform API, install Pulse Agent, or use both for full coverage.',
|
||||
'setup.welcome.hero.step.admin': 'Create the admin account',
|
||||
'setup.welcome.hero.step.source': 'Choose the first source',
|
||||
'setup.welcome.hero.step.unlock': 'Unlock this Pulse server',
|
||||
'setup.welcome.hero.stepsIntro': 'Three steps:',
|
||||
'setup.welcome.hero.stepsThen': 'then',
|
||||
'setup.welcome.hero.title': 'Welcome to Pulse',
|
||||
'setup.welcome.placeholder.bootstrapToken': 'Paste your bootstrap token',
|
||||
'setup.welcome.success.commandCopied': 'Command copied to clipboard',
|
||||
'setup.welcome.tokenHelp.afterVerify':
|
||||
'After Pulse verifies this token, the next step is creating the admin account for this server.',
|
||||
'setup.welcome.tokenHelp.docker':
|
||||
'This one-time bootstrap token only unlocks first-run setup. Run the command above and paste the token string it prints. After verification, you will create the admin account and Pulse will generate the long-lived API token separately.',
|
||||
'setup.welcome.tokenHelp.host':
|
||||
'This one-time bootstrap token only unlocks first-run setup on this Pulse server. Run the command above and paste the token string it prints. It is not your admin password and it is not the API token you will use after setup.',
|
||||
'setup.welcome.tokenHelp.title': 'What this token does',
|
||||
'setup.welcome.unlockTitle': 'Unlock setup',
|
||||
'settings.general.appearance.title': 'Appearance',
|
||||
'settings.general.fullWidth.description':
|
||||
'Expand content to use all available screen width on large monitors.',
|
||||
@@ -239,10 +373,255 @@ export const SETTINGS_GENERAL_MIGRATED_MESSAGE_KEYS = [
|
||||
'settings.general.theme.title',
|
||||
] as const satisfies readonly I18nMessageKey[];
|
||||
|
||||
export const FIRST_SESSION_MONITORING_MIGRATED_MESSAGE_KEYS = [
|
||||
'runtimeHome.openingWorkspace',
|
||||
'setup.completion.action.addInfrastructure',
|
||||
'setup.completion.action.downloadCredentials',
|
||||
'setup.completion.action.installAgent',
|
||||
'setup.completion.action.openInfrastructure',
|
||||
'setup.completion.connectedSummary.singular',
|
||||
'setup.completion.connectedSummary.plural',
|
||||
'setup.completion.credentials.apiTokenLabel',
|
||||
'setup.completion.credentials.badge',
|
||||
'setup.completion.credentials.continuation.empty',
|
||||
'setup.completion.credentials.continuation.connected',
|
||||
'setup.completion.credentials.description',
|
||||
'setup.completion.credentials.passwordLabel',
|
||||
'setup.completion.credentials.title',
|
||||
'setup.completion.credentials.usernameLabel',
|
||||
'setup.completion.download.content',
|
||||
'setup.completion.hero.connected.description.agent',
|
||||
'setup.completion.hero.connected.description.api',
|
||||
'setup.completion.hero.connected.description.both',
|
||||
'setup.completion.hero.connected.title',
|
||||
'setup.completion.hero.empty.description',
|
||||
'setup.completion.hero.empty.title',
|
||||
'setup.completion.nextStep.ariaLabel',
|
||||
'setup.completion.nextStep.badge',
|
||||
'setup.completion.nextStep.detail.agent',
|
||||
'setup.completion.nextStep.detail.api',
|
||||
'setup.completion.nextStep.detail.both',
|
||||
'setup.completion.nextStep.detail.empty',
|
||||
'setup.completion.nextStep.label',
|
||||
'setup.completion.nextStep.summary.connected.singular',
|
||||
'setup.completion.nextStep.summary.connected.plural',
|
||||
'setup.completion.nextStep.summary.empty',
|
||||
'setup.completion.nextStep.title.connected',
|
||||
'setup.completion.nextStep.title.empty',
|
||||
'setup.completion.resource.unknownName',
|
||||
'setup.completion.sourceOptions.agent.description',
|
||||
'setup.completion.sourceOptions.agent.title',
|
||||
'setup.completion.sourceOptions.both.description',
|
||||
'setup.completion.sourceOptions.both.title',
|
||||
'setup.completion.sourceOptions.platformApi.description',
|
||||
'setup.completion.sourceOptions.platformApi.title',
|
||||
'setup.completion.sourceOptions.title',
|
||||
'setup.progress.ariaLabel',
|
||||
'setup.progress.state.completed',
|
||||
'setup.progress.state.current',
|
||||
'setup.progress.stepAriaLabel',
|
||||
'setup.security.action.back',
|
||||
'setup.security.action.createAccount',
|
||||
'setup.security.action.settingUp',
|
||||
'setup.security.customPassword',
|
||||
'setup.security.description',
|
||||
'setup.security.error.passwordMismatch',
|
||||
'setup.security.error.passwordRequired',
|
||||
'setup.security.error.passwordTooShort',
|
||||
'setup.security.error.setupFailed',
|
||||
'setup.security.generatedPasswordHelp',
|
||||
'setup.security.label.confirmPassword',
|
||||
'setup.security.label.password',
|
||||
'setup.security.label.username',
|
||||
'setup.security.minimumPasswordHelp',
|
||||
'setup.security.nextScreen.itemApiToken',
|
||||
'setup.security.nextScreen.itemCredentials',
|
||||
'setup.security.nextScreen.saveOnce',
|
||||
'setup.security.nextScreen.title',
|
||||
'setup.security.passwordMode.autoGenerate',
|
||||
'setup.security.placeholder.password',
|
||||
'setup.security.placeholder.username',
|
||||
'setup.security.showPassword.hide',
|
||||
'setup.security.showPassword.show',
|
||||
'setup.security.title',
|
||||
'setup.step.firstSource',
|
||||
'setup.step.security',
|
||||
'setup.step.unlockServer',
|
||||
'setup.wizard.ariaLabel',
|
||||
'setup.welcome.action.continueSecurity',
|
||||
'setup.welcome.action.verifyToken',
|
||||
'setup.welcome.action.verifyingToken',
|
||||
'setup.welcome.copyCommandTitle',
|
||||
'setup.welcome.deploymentHint.containerized',
|
||||
'setup.welcome.deploymentHint.direct',
|
||||
'setup.welcome.deploymentHint.dockerNamed',
|
||||
'setup.welcome.deploymentHint.dockerUnnamed',
|
||||
'setup.welcome.deploymentHint.lxc',
|
||||
'setup.welcome.deploymentLabel.containerized',
|
||||
'setup.welcome.deploymentLabel.direct',
|
||||
'setup.welcome.deploymentLabel.docker',
|
||||
'setup.welcome.deploymentLabel.lxc',
|
||||
'setup.welcome.error.copyCommandFailed',
|
||||
'setup.welcome.error.invalidBootstrapToken',
|
||||
'setup.welcome.error.invalidBootstrapTokenResponse',
|
||||
'setup.welcome.error.missingBootstrapToken',
|
||||
'setup.welcome.error.snapshotPaste',
|
||||
'setup.welcome.hero.coverage',
|
||||
'setup.welcome.hero.step.admin',
|
||||
'setup.welcome.hero.step.source',
|
||||
'setup.welcome.hero.step.unlock',
|
||||
'setup.welcome.hero.stepsIntro',
|
||||
'setup.welcome.hero.stepsThen',
|
||||
'setup.welcome.hero.title',
|
||||
'setup.welcome.placeholder.bootstrapToken',
|
||||
'setup.welcome.success.commandCopied',
|
||||
'setup.welcome.tokenHelp.afterVerify',
|
||||
'setup.welcome.tokenHelp.docker',
|
||||
'setup.welcome.tokenHelp.host',
|
||||
'setup.welcome.tokenHelp.title',
|
||||
] as const satisfies readonly I18nMessageKey[];
|
||||
|
||||
export const I18N_MESSAGES: I18nCatalogs = {
|
||||
en: EN_MESSAGES,
|
||||
de: {
|
||||
...EN_MESSAGES,
|
||||
'runtimeHome.openingWorkspace': 'Arbeitsbereich wird geoeffnet...',
|
||||
'setup.completion.action.addInfrastructure': 'Infrastruktur hinzufuegen',
|
||||
'setup.completion.action.downloadCredentials': 'Zugangsdaten herunterladen',
|
||||
'setup.completion.action.installAgent': 'Pulse Agent installieren',
|
||||
'setup.completion.action.openInfrastructure': 'Infrastruktur oeffnen',
|
||||
'setup.completion.connectedSummary.singular': 'Verbunden ({count} System)',
|
||||
'setup.completion.connectedSummary.plural': 'Verbunden ({count} Systeme)',
|
||||
'setup.completion.credentials.apiTokenLabel': 'Admin-API-Token',
|
||||
'setup.completion.credentials.badge': 'Waehrend der Einrichtung angezeigt',
|
||||
'setup.completion.credentials.continuation.empty': 'Infrastruktur hinzufuegen.',
|
||||
'setup.completion.credentials.continuation.connected':
|
||||
'Infrastruktur oder Infrastruktur hinzufuegen.',
|
||||
'setup.completion.credentials.description':
|
||||
'Speichern Sie Admin-Anmeldung und API-Token, bevor Sie diesen Bildschirm verlassen, und fahren Sie dann mit {destination} fort',
|
||||
'setup.completion.credentials.passwordLabel': 'Passwort',
|
||||
'setup.completion.credentials.title': 'Zugangsdaten jetzt speichern',
|
||||
'setup.completion.credentials.usernameLabel': 'Benutzername',
|
||||
'setup.completion.download.content':
|
||||
'Pulse-Zugangsdaten\n==================\nErstellt: {generatedAt}\n\nWeb-Anmeldung:\n----------\nURL: {baseUrl}\nBenutzername: {username}\nPasswort: {password}\n\nAdmin-API-Token:\n----------------\n{apiToken}\n\nInfrastruktur:\n---------------\n{infrastructureUrl}\n\nVerwenden Sie Infrastruktur hinzufuegen, um eine Plattform-API, Pulse Agent oder beides\nfuer das erste System auszuwaehlen, das Pulse ueberwachen soll.\n\nBewahren Sie diese Zugangsdaten sicher auf!\n',
|
||||
'setup.completion.hero.connected.description.agent':
|
||||
'Ihr Admin-Konto ist bereit und Pulse empfaengt bereits Telemetrie. Oeffnen Sie Infrastruktur, um das erste System zu pruefen, und kehren Sie zu Infrastruktur hinzufuegen zurueck, wenn Sie eine weitere Pulse Agent- oder Plattform-API-Quelle wollen.',
|
||||
'setup.completion.hero.connected.description.api':
|
||||
'Ihr Admin-Konto ist bereit und Pulse empfaengt bereits Telemetrie. Oeffnen Sie Infrastruktur, um das erste System zu pruefen, und kehren Sie zu Infrastruktur hinzufuegen zurueck, wenn Sie eine weitere Plattform-API- oder Pulse Agent-Quelle wollen.',
|
||||
'setup.completion.hero.connected.description.both':
|
||||
'Ihr Admin-Konto ist bereit und Pulse empfaengt bereits Telemetrie. Oeffnen Sie Infrastruktur, um das erste System zu pruefen, und kehren Sie zu Infrastruktur hinzufuegen zurueck, wenn Sie eine weitere Plattform-API- oder Agent-Quelle wollen.',
|
||||
'setup.completion.hero.connected.title': 'Erstes ueberwachtes System verbunden',
|
||||
'setup.completion.hero.empty.description':
|
||||
'Ihr Admin-Konto ist bereit. Waehlen Sie als Naechstes, wie das erste System in das einheitliche Infrastrukturmodell gelangt: Plattform-API-Inventar, Pulse Agent-Telemetrie oder beides.',
|
||||
'setup.completion.hero.empty.title': 'Erste Infrastrukturquelle waehlen',
|
||||
'setup.completion.nextStep.ariaLabel': 'Naechster Einrichtungsschritt',
|
||||
'setup.completion.nextStep.badge': 'Empfohlener naechster Schritt',
|
||||
'setup.completion.nextStep.detail.agent':
|
||||
'Infrastruktur hinzufuegen bleibt fuer weitere Pulse Agent-Systeme oder Plattform-API-Inventar verfuegbar, wenn eine Plattform die Umgebung verwaltet.',
|
||||
'setup.completion.nextStep.detail.api':
|
||||
'Infrastruktur hinzufuegen bleibt fuer weitere API-gestuetzte Systeme oder Pulse Agent-Telemetrie verfuegbar, wenn ein System lokale Knotenabdeckung braucht.',
|
||||
'setup.completion.nextStep.detail.both':
|
||||
'Infrastruktur hinzufuegen bleibt jederzeit verfuegbar, wenn Sie dieses erste System um eine weitere API-Quelle, Agent-Quelle oder beides erweitern wollen.',
|
||||
'setup.completion.nextStep.detail.empty':
|
||||
'Beginnen Sie mit einer Plattform-API, wenn eine Plattform die Umgebung verwaltet. Installieren Sie Pulse Agent, wenn das System selbst lokale Knotentelemetrie melden soll.',
|
||||
'setup.completion.nextStep.label': 'Naechster Schritt',
|
||||
'setup.completion.nextStep.summary.connected.singular':
|
||||
'Oeffnen Sie Infrastruktur, um Ihr erstes verbundenes System zu pruefen.',
|
||||
'setup.completion.nextStep.summary.connected.plural':
|
||||
'Oeffnen Sie Infrastruktur, um Ihre verbundenen Systeme zu pruefen.',
|
||||
'setup.completion.nextStep.summary.empty':
|
||||
'Oeffnen Sie Infrastruktur hinzufuegen, um eine Plattform-API, Pulse Agent oder beides zu waehlen.',
|
||||
'setup.completion.nextStep.title.connected': 'Infrastruktur oeffnen',
|
||||
'setup.completion.nextStep.title.empty': 'Strategie fuer erste Quelle waehlen',
|
||||
'setup.completion.resource.unknownName': 'Unbekannt',
|
||||
'setup.completion.sourceOptions.agent.description':
|
||||
'Knotenlokale Telemetrie fuer eigenstaendige Hosts, Dienste, Docker und Kubernetes.',
|
||||
'setup.completion.sourceOptions.agent.title': 'Pulse Agent',
|
||||
'setup.completion.sourceOptions.both.description':
|
||||
'Kombinieren Sie Plattforminventar mit Agent-Telemetrie, wenn vollstaendige Abdeckung wichtig ist.',
|
||||
'setup.completion.sourceOptions.both.title': 'Beides verwenden',
|
||||
'setup.completion.sourceOptions.platformApi.description':
|
||||
'Inventar und Zustand aus Proxmox, TrueNAS, VMware, PBS oder PMG.',
|
||||
'setup.completion.sourceOptions.platformApi.title': 'Plattform-API',
|
||||
'setup.completion.sourceOptions.title': 'Quellenoptionen',
|
||||
'setup.progress.ariaLabel': 'Einrichtungsfortschritt',
|
||||
'setup.progress.state.completed': ', abgeschlossen',
|
||||
'setup.progress.state.current': ', aktuell',
|
||||
'setup.progress.stepAriaLabel': 'Schritt {index}: {step}{state}',
|
||||
'setup.security.action.back': 'Zurueck',
|
||||
'setup.security.action.createAccount': 'Konto erstellen und fortfahren',
|
||||
'setup.security.action.settingUp': 'Einrichtung laeuft...',
|
||||
'setup.security.customPassword': 'Eigenes Passwort',
|
||||
'setup.security.description':
|
||||
'Der naechste Schritt ist die Auswahl der ersten Infrastrukturquelle.',
|
||||
'setup.security.error.passwordMismatch': 'Passwoerter stimmen nicht ueberein',
|
||||
'setup.security.error.passwordRequired': 'Bitte geben Sie ein Passwort ein',
|
||||
'setup.security.error.passwordTooShort': 'Das Passwort muss mindestens 12 Zeichen haben',
|
||||
'setup.security.error.setupFailed': 'Einrichtung fehlgeschlagen: {error}',
|
||||
'setup.security.generatedPasswordHelp':
|
||||
'Ein sicheres 20-Zeichen-Passwort wird erzeugt und auf dem naechsten Bildschirm angezeigt.',
|
||||
'setup.security.label.confirmPassword': 'Passwort bestaetigen',
|
||||
'setup.security.label.password': 'Passwort',
|
||||
'setup.security.label.username': 'Benutzername',
|
||||
'setup.security.minimumPasswordHelp': 'Mindestens 12 Zeichen.',
|
||||
'setup.security.nextScreen.itemApiToken': 'Ein Admin-API-Token fuer Automatisierung',
|
||||
'setup.security.nextScreen.itemCredentials': 'Ihr Benutzername und Passwort',
|
||||
'setup.security.nextScreen.saveOnce':
|
||||
'Speichern Sie sie vor dem Fortfahren; sie werden nur einmal angezeigt.',
|
||||
'setup.security.nextScreen.title': 'Auf dem naechsten Bildschirm',
|
||||
'setup.security.passwordMode.autoGenerate': 'Automatisch erzeugen',
|
||||
'setup.security.placeholder.password': 'Passwort (mind. 12 Zeichen)',
|
||||
'setup.security.placeholder.username': 'admin',
|
||||
'setup.security.showPassword.hide': 'Ausblenden',
|
||||
'setup.security.showPassword.show': 'Anzeigen',
|
||||
'setup.security.title': 'Admin-Konto erstellen',
|
||||
'setup.step.firstSource': 'Erste Quelle',
|
||||
'setup.step.security': 'Sicherheit',
|
||||
'setup.step.unlockServer': 'Server entsperren',
|
||||
'setup.wizard.ariaLabel': 'Pulse-Einrichtungsassistent',
|
||||
'setup.welcome.action.continueSecurity': 'Weiter zu Sicherheit',
|
||||
'setup.welcome.action.verifyToken': 'Bootstrap-Token pruefen',
|
||||
'setup.welcome.action.verifyingToken': 'Bootstrap-Token wird geprueft...',
|
||||
'setup.welcome.copyCommandTitle': 'Befehl kopieren',
|
||||
'setup.welcome.deploymentHint.containerized':
|
||||
'Pulse scheint in einer containerisierten Umgebung zu laufen. Fuehren Sie den Befehl auf dem Host aus, der den Container verwaltet, damit Sie das einmalige Setup-Token ausgeben koennen.',
|
||||
'setup.welcome.deploymentHint.direct':
|
||||
'Fuehren Sie den Befehl direkt in einer Shell auf dem Pulse-Server aus, um das einmalige Setup-Token auszugeben.',
|
||||
'setup.welcome.deploymentHint.dockerNamed':
|
||||
'Pulse scheint in Docker als Container "{containerName}" zu laufen. Fuehren Sie den Befehl auf dem Docker-Host aus, um das einmalige Setup-Token aus diesem Container auszugeben.',
|
||||
'setup.welcome.deploymentHint.dockerUnnamed':
|
||||
'Pulse scheint in Docker zu laufen. Fuehren Sie den Befehl auf dem Docker-Host aus und ersetzen Sie <pulse-container> durch den Namen des laufenden Pulse-Containers.',
|
||||
'setup.welcome.deploymentHint.lxc':
|
||||
'Pulse scheint in LXC-Container {ctid} zu laufen. Fuehren Sie den Befehl auf dem Proxmox-Host aus, um in diesen Container zu wechseln und das einmalige Setup-Token auszugeben.',
|
||||
'setup.welcome.deploymentLabel.containerized': 'Containerisierte Bereitstellung',
|
||||
'setup.welcome.deploymentLabel.direct': 'Direkte Host-Installation',
|
||||
'setup.welcome.deploymentLabel.docker': 'Docker-Bereitstellung',
|
||||
'setup.welcome.deploymentLabel.lxc': 'LXC-Container',
|
||||
'setup.welcome.error.copyCommandFailed': 'Befehl konnte nicht kopiert werden',
|
||||
'setup.welcome.error.invalidBootstrapToken':
|
||||
'Ungueltiges Bootstrap-Token. Bitte pruefen und erneut versuchen.',
|
||||
'setup.welcome.error.invalidBootstrapTokenResponse': 'Ungueltiges Bootstrap-Token',
|
||||
'setup.welcome.error.missingBootstrapToken': 'Bitte geben Sie das Bootstrap-Token ein',
|
||||
'setup.welcome.error.snapshotPaste':
|
||||
'Das sieht nach dem verschluesselten Inhalt der Datei .bootstrap_token aus, nicht nach dem rohen Setup-Token. Fuehren Sie den obigen Befehl aus und fuegen Sie die ausgegebene Token-Zeichenfolge ein.',
|
||||
'setup.welcome.hero.coverage':
|
||||
'Verbinden Sie eine Plattform-API, installieren Sie Pulse Agent oder verwenden Sie beides fuer vollstaendige Abdeckung.',
|
||||
'setup.welcome.hero.step.admin': 'Admin-Konto erstellen',
|
||||
'setup.welcome.hero.step.source': 'Erste Quelle waehlen',
|
||||
'setup.welcome.hero.step.unlock': 'Diesen Pulse-Server entsperren',
|
||||
'setup.welcome.hero.stepsIntro': 'Drei Schritte:',
|
||||
'setup.welcome.hero.stepsThen': 'dann',
|
||||
'setup.welcome.hero.title': 'Willkommen bei Pulse',
|
||||
'setup.welcome.placeholder.bootstrapToken': 'Bootstrap-Token einfuegen',
|
||||
'setup.welcome.success.commandCopied': 'Befehl in Zwischenablage kopiert',
|
||||
'setup.welcome.tokenHelp.afterVerify':
|
||||
'Nachdem Pulse dieses Token geprueft hat, erstellen Sie im naechsten Schritt das Admin-Konto fuer diesen Server.',
|
||||
'setup.welcome.tokenHelp.docker':
|
||||
'Dieses einmalige Bootstrap-Token entsperrt nur die Ersteinrichtung. Fuehren Sie den obigen Befehl aus und fuegen Sie die ausgegebene Token-Zeichenfolge ein. Nach der Pruefung erstellen Sie das Admin-Konto, und Pulse erzeugt das langlebige API-Token separat.',
|
||||
'setup.welcome.tokenHelp.host':
|
||||
'Dieses einmalige Bootstrap-Token entsperrt nur die Ersteinrichtung auf diesem Pulse-Server. Fuehren Sie den obigen Befehl aus und fuegen Sie die ausgegebene Token-Zeichenfolge ein. Es ist nicht Ihr Admin-Passwort und nicht das API-Token, das Sie nach der Einrichtung verwenden.',
|
||||
'setup.welcome.tokenHelp.title': 'Was dieses Token macht',
|
||||
'setup.welcome.unlockTitle': 'Einrichtung entsperren',
|
||||
'settings.general.appearance.title': 'Darstellung',
|
||||
'settings.general.fullWidth.description':
|
||||
'Nutzen Sie auf grossen Monitoren die gesamte verfuegbare Bildschirmbreite.',
|
||||
@@ -428,6 +807,143 @@ export const I18N_MESSAGES: I18nCatalogs = {
|
||||
},
|
||||
es: {
|
||||
...EN_MESSAGES,
|
||||
'runtimeHome.openingWorkspace': 'Abriendo el espacio de trabajo...',
|
||||
'setup.completion.action.addInfrastructure': 'Agregar infraestructura',
|
||||
'setup.completion.action.downloadCredentials': 'Descargar credenciales',
|
||||
'setup.completion.action.installAgent': 'Instalar Pulse Agent',
|
||||
'setup.completion.action.openInfrastructure': 'Abrir Infraestructura',
|
||||
'setup.completion.connectedSummary.singular': 'Conectado ({count} sistema)',
|
||||
'setup.completion.connectedSummary.plural': 'Conectados ({count} sistemas)',
|
||||
'setup.completion.credentials.apiTokenLabel': 'Token de API de administrador',
|
||||
'setup.completion.credentials.badge': 'Se muestra durante la configuración',
|
||||
'setup.completion.credentials.continuation.empty': 'Agregar infraestructura.',
|
||||
'setup.completion.credentials.continuation.connected':
|
||||
'Infraestructura o Agregar infraestructura.',
|
||||
'setup.completion.credentials.description':
|
||||
'Guarda el inicio de sesión de administrador y el token de API antes de salir de esta pantalla; luego continúa en {destination}',
|
||||
'setup.completion.credentials.passwordLabel': 'Contraseña',
|
||||
'setup.completion.credentials.title': 'Credenciales que debes guardar ahora',
|
||||
'setup.completion.credentials.usernameLabel': 'Usuario',
|
||||
'setup.completion.download.content':
|
||||
'Credenciales de Pulse\n==================\nGenerado: {generatedAt}\n\nInicio de sesión web:\n----------\nURL: {baseUrl}\nUsuario: {username}\nContraseña: {password}\n\nToken de API de administrador:\n----------------\n{apiToken}\n\nInfraestructura:\n---------------\n{infrastructureUrl}\n\nUsa Agregar infraestructura para elegir una API de plataforma, Pulse Agent o ambos\npara el primer sistema que Pulse debe supervisar.\n\nGuarda estas credenciales en un lugar seguro.\n',
|
||||
'setup.completion.hero.connected.description.agent':
|
||||
'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema; luego vuelve a Agregar infraestructura cuando quieras otra fuente Pulse Agent o API de plataforma.',
|
||||
'setup.completion.hero.connected.description.api':
|
||||
'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema; luego vuelve a Agregar infraestructura cuando quieras otra API de plataforma o fuente Pulse Agent.',
|
||||
'setup.completion.hero.connected.description.both':
|
||||
'Tu cuenta de administrador está lista y Pulse ya recibe telemetría. Abre Infraestructura para revisar el primer sistema; luego vuelve a Agregar infraestructura cuando quieras otra API de plataforma, fuente Agent o ambas.',
|
||||
'setup.completion.hero.connected.title': 'Primer sistema supervisado conectado',
|
||||
'setup.completion.hero.empty.description':
|
||||
'Tu cuenta de administrador está lista. Ahora elige cómo entrará el primer sistema en el modelo unificado de infraestructura: inventario por API de plataforma, telemetría de Pulse Agent o ambos.',
|
||||
'setup.completion.hero.empty.title': 'Elige tu primera fuente de infraestructura',
|
||||
'setup.completion.nextStep.ariaLabel': 'Siguiente paso de configuración',
|
||||
'setup.completion.nextStep.badge': 'Siguiente paso recomendado',
|
||||
'setup.completion.nextStep.detail.agent':
|
||||
'Agregar infraestructura queda disponible para más sistemas Pulse Agent o inventario por API de plataforma cuando una plataforma administra el entorno.',
|
||||
'setup.completion.nextStep.detail.api':
|
||||
'Agregar infraestructura queda disponible para más sistemas respaldados por API o telemetría de Pulse Agent cuando un sistema necesita cobertura local del nodo.',
|
||||
'setup.completion.nextStep.detail.both':
|
||||
'Agregar infraestructura queda disponible cuando quieras ampliar este primer sistema con otra fuente API, fuente Agent o ambas.',
|
||||
'setup.completion.nextStep.detail.empty':
|
||||
'Empieza con una API de plataforma cuando una plataforma administra el entorno. Instala Pulse Agent cuando el propio sistema deba informar telemetría local del nodo.',
|
||||
'setup.completion.nextStep.label': 'Siguiente paso',
|
||||
'setup.completion.nextStep.summary.connected.singular':
|
||||
'Abre Infraestructura para revisar tu primer sistema conectado.',
|
||||
'setup.completion.nextStep.summary.connected.plural':
|
||||
'Abre Infraestructura para revisar tus sistemas conectados.',
|
||||
'setup.completion.nextStep.summary.empty':
|
||||
'Abre Agregar infraestructura para elegir una API de plataforma, Pulse Agent o ambos.',
|
||||
'setup.completion.nextStep.title.connected': 'Abrir Infraestructura',
|
||||
'setup.completion.nextStep.title.empty': 'Elige la estrategia de primera fuente',
|
||||
'setup.completion.resource.unknownName': 'Desconocido',
|
||||
'setup.completion.sourceOptions.agent.description':
|
||||
'Telemetría local del nodo para hosts independientes, servicios, Docker y Kubernetes.',
|
||||
'setup.completion.sourceOptions.agent.title': 'Pulse Agent',
|
||||
'setup.completion.sourceOptions.both.description':
|
||||
'Combina inventario de plataforma con telemetría de Agent cuando importa la cobertura completa.',
|
||||
'setup.completion.sourceOptions.both.title': 'Usar ambos',
|
||||
'setup.completion.sourceOptions.platformApi.description':
|
||||
'Inventario y salud desde Proxmox, TrueNAS, VMware, PBS o PMG.',
|
||||
'setup.completion.sourceOptions.platformApi.title': 'API de plataforma',
|
||||
'setup.completion.sourceOptions.title': 'Opciones de fuente',
|
||||
'setup.progress.ariaLabel': 'Progreso de configuración',
|
||||
'setup.progress.state.completed': ', completado',
|
||||
'setup.progress.state.current': ', actual',
|
||||
'setup.progress.stepAriaLabel': 'Paso {index}: {step}{state}',
|
||||
'setup.security.action.back': 'Atrás',
|
||||
'setup.security.action.createAccount': 'Crear cuenta y continuar',
|
||||
'setup.security.action.settingUp': 'Configurando...',
|
||||
'setup.security.customPassword': 'Contraseña personalizada',
|
||||
'setup.security.description':
|
||||
'El siguiente paso es elegir la primera fuente de infraestructura.',
|
||||
'setup.security.error.passwordMismatch': 'Las contraseñas no coinciden',
|
||||
'setup.security.error.passwordRequired': 'Ingresa una contraseña',
|
||||
'setup.security.error.passwordTooShort': 'La contraseña debe tener al menos 12 caracteres',
|
||||
'setup.security.error.setupFailed': 'La configuración falló: {error}',
|
||||
'setup.security.generatedPasswordHelp':
|
||||
'Se generará una contraseña segura de 20 caracteres y se mostrará en la siguiente pantalla.',
|
||||
'setup.security.label.confirmPassword': 'Confirmar contraseña',
|
||||
'setup.security.label.password': 'Contraseña',
|
||||
'setup.security.label.username': 'Usuario',
|
||||
'setup.security.minimumPasswordHelp': 'Mínimo 12 caracteres.',
|
||||
'setup.security.nextScreen.itemApiToken':
|
||||
'Un token de API de administrador para automatización',
|
||||
'setup.security.nextScreen.itemCredentials': 'Tu usuario y contraseña',
|
||||
'setup.security.nextScreen.saveOnce': 'Guárdalos antes de continuar; se muestran una sola vez.',
|
||||
'setup.security.nextScreen.title': 'En la siguiente pantalla',
|
||||
'setup.security.passwordMode.autoGenerate': 'Generar automáticamente',
|
||||
'setup.security.placeholder.password': 'Contraseña (mínimo 12 caracteres)',
|
||||
'setup.security.placeholder.username': 'admin',
|
||||
'setup.security.showPassword.hide': 'Ocultar',
|
||||
'setup.security.showPassword.show': 'Mostrar',
|
||||
'setup.security.title': 'Crear tu cuenta de administrador',
|
||||
'setup.step.firstSource': 'Primera fuente',
|
||||
'setup.step.security': 'Seguridad',
|
||||
'setup.step.unlockServer': 'Desbloquear servidor',
|
||||
'setup.wizard.ariaLabel': 'Asistente de configuración de Pulse',
|
||||
'setup.welcome.action.continueSecurity': 'Continuar a Seguridad',
|
||||
'setup.welcome.action.verifyToken': 'Verificar token de bootstrap',
|
||||
'setup.welcome.action.verifyingToken': 'Verificando token de bootstrap...',
|
||||
'setup.welcome.copyCommandTitle': 'Copiar comando',
|
||||
'setup.welcome.deploymentHint.containerized':
|
||||
'Pulse parece ejecutarse en un entorno contenedorizado. Ejecuta el comando desde el host que administra el contenedor para imprimir el token de configuración de un solo uso.',
|
||||
'setup.welcome.deploymentHint.direct':
|
||||
'Ejecuta el comando directamente en una shell del servidor Pulse para imprimir el token de configuración de un solo uso.',
|
||||
'setup.welcome.deploymentHint.dockerNamed':
|
||||
'Pulse parece ejecutarse en Docker como contenedor "{containerName}". Ejecuta el comando en el host Docker para imprimir el token de configuración de un solo uso desde ese contenedor.',
|
||||
'setup.welcome.deploymentHint.dockerUnnamed':
|
||||
'Pulse parece ejecutarse en Docker. Ejecuta el comando en el host Docker y reemplaza <pulse-container> por el nombre del contenedor Pulse en ejecución.',
|
||||
'setup.welcome.deploymentHint.lxc':
|
||||
'Pulse parece ejecutarse en el contenedor LXC {ctid}. Ejecuta el comando en el host Proxmox para entrar en ese contenedor e imprimir el token de configuración de un solo uso.',
|
||||
'setup.welcome.deploymentLabel.containerized': 'Despliegue contenedorizado',
|
||||
'setup.welcome.deploymentLabel.direct': 'Instalación directa en host',
|
||||
'setup.welcome.deploymentLabel.docker': 'Despliegue Docker',
|
||||
'setup.welcome.deploymentLabel.lxc': 'Contenedor LXC',
|
||||
'setup.welcome.error.copyCommandFailed': 'No se pudo copiar el comando',
|
||||
'setup.welcome.error.invalidBootstrapToken':
|
||||
'Token de bootstrap no válido. Revísalo e inténtalo de nuevo.',
|
||||
'setup.welcome.error.invalidBootstrapTokenResponse': 'Token de bootstrap no válido',
|
||||
'setup.welcome.error.missingBootstrapToken': 'Ingresa el token de bootstrap',
|
||||
'setup.welcome.error.snapshotPaste':
|
||||
'Eso parece el contenido cifrado del archivo .bootstrap_token, no el token de configuración sin procesar. Ejecuta el comando anterior y pega la cadena de token que imprime.',
|
||||
'setup.welcome.hero.coverage':
|
||||
'Conecta una API de plataforma, instala Pulse Agent o usa ambos para cobertura completa.',
|
||||
'setup.welcome.hero.step.admin': 'Crear la cuenta de administrador',
|
||||
'setup.welcome.hero.step.source': 'Elegir la primera fuente',
|
||||
'setup.welcome.hero.step.unlock': 'Desbloquear este servidor Pulse',
|
||||
'setup.welcome.hero.stepsIntro': 'Tres pasos:',
|
||||
'setup.welcome.hero.stepsThen': 'luego',
|
||||
'setup.welcome.hero.title': 'Bienvenido a Pulse',
|
||||
'setup.welcome.placeholder.bootstrapToken': 'Pega tu token de bootstrap',
|
||||
'setup.welcome.success.commandCopied': 'Comando copiado al portapapeles',
|
||||
'setup.welcome.tokenHelp.afterVerify':
|
||||
'Después de que Pulse verifique este token, el siguiente paso es crear la cuenta de administrador para este servidor.',
|
||||
'setup.welcome.tokenHelp.docker':
|
||||
'Este token de bootstrap de un solo uso solo desbloquea la configuración inicial. Ejecuta el comando anterior y pega la cadena de token que imprime. Después de la verificación, crearás la cuenta de administrador y Pulse generará el token de API de larga duración por separado.',
|
||||
'setup.welcome.tokenHelp.host':
|
||||
'Este token de bootstrap de un solo uso solo desbloquea la configuración inicial en este servidor Pulse. Ejecuta el comando anterior y pega la cadena de token que imprime. No es tu contraseña de administrador ni el token de API que usarás después de la configuración.',
|
||||
'setup.welcome.tokenHelp.title': 'Qué hace este token',
|
||||
'setup.welcome.unlockTitle': 'Desbloquear configuración',
|
||||
'settings.general.appearance.title': 'Apariencia',
|
||||
'settings.general.fullWidth.description':
|
||||
'Expande el contenido para usar todo el ancho disponible en monitores grandes.',
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
SUPPORTED_LOCALES,
|
||||
type SupportedLocale,
|
||||
} from './locales';
|
||||
import { SETTINGS_GENERAL_MIGRATED_MESSAGE_KEYS, type I18nMessageKey } from './messages';
|
||||
import {
|
||||
FIRST_SESSION_MONITORING_MIGRATED_MESSAGE_KEYS,
|
||||
SETTINGS_GENERAL_MIGRATED_MESSAGE_KEYS,
|
||||
type I18nMessageKey,
|
||||
} from './messages';
|
||||
|
||||
export const LOCALIZATION_FOUNDATION = {
|
||||
ownerLayer: 'frontend-modern/src/i18n',
|
||||
@@ -38,10 +42,39 @@ export const SETTINGS_GENERAL_NON_TRANSLATABLE_TOKENS = [
|
||||
'"Update"',
|
||||
] as const;
|
||||
|
||||
export const FIRST_SESSION_MONITORING_NON_TRANSLATABLE_TOKENS = [
|
||||
'Pulse',
|
||||
'Pulse Agent',
|
||||
'API',
|
||||
'URL',
|
||||
'Docker',
|
||||
'Kubernetes',
|
||||
'Proxmox',
|
||||
'TrueNAS',
|
||||
'VMware',
|
||||
'PBS',
|
||||
'PMG',
|
||||
'LXC',
|
||||
'.bootstrap_token',
|
||||
'<pulse-container>',
|
||||
'<ctid>',
|
||||
'sudo pulse bootstrap-token',
|
||||
'docker exec',
|
||||
'pct exec',
|
||||
] as const;
|
||||
|
||||
export const LOCALIZED_SETTINGS_GENERAL_JOURNEY_KEYS =
|
||||
SETTINGS_GENERAL_MIGRATED_MESSAGE_KEYS satisfies readonly I18nMessageKey[];
|
||||
|
||||
export const LOCALIZED_FIRST_SESSION_MONITORING_JOURNEY_KEYS =
|
||||
FIRST_SESSION_MONITORING_MIGRATED_MESSAGE_KEYS satisfies readonly I18nMessageKey[];
|
||||
|
||||
export const SETTINGS_GENERAL_ALLOWED_IDENTICAL_TRANSLATIONS = {
|
||||
de: ['settings.nav.group.system', 'settings.general.theme.option.system'],
|
||||
es: ['settings.header.systemGeneral.title', 'settings.nav.item.general'],
|
||||
} as const satisfies Partial<Record<SupportedLocale, readonly I18nMessageKey[]>>;
|
||||
|
||||
export const FIRST_SESSION_MONITORING_ALLOWED_IDENTICAL_TRANSLATIONS = {
|
||||
de: ['setup.completion.sourceOptions.agent.title', 'setup.security.placeholder.username'],
|
||||
es: ['setup.completion.sourceOptions.agent.title', 'setup.security.placeholder.username'],
|
||||
} as const satisfies Partial<Record<SupportedLocale, readonly I18nMessageKey[]>>;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { t } from '@/i18n';
|
||||
|
||||
export default function RuntimeHome() {
|
||||
return <div class="px-4 py-6 text-sm text-muted">Opening workspace...</div>;
|
||||
return <div class="px-4 py-6 text-sm text-muted">{t('runtimeHome.openingWorkspace')}</div>;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import { render, screen } from '@solidjs/testing-library';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import { DEFAULT_LOCALE, setActiveLocale } from '@/i18n';
|
||||
import RuntimeHome from '@/pages/RuntimeHome';
|
||||
|
||||
describe('RuntimeHome', () => {
|
||||
afterEach(() => {
|
||||
setActiveLocale(DEFAULT_LOCALE);
|
||||
});
|
||||
|
||||
it('defers workspace routing to the authenticated app shell', () => {
|
||||
render(() => <RuntimeHome />);
|
||||
|
||||
expect(screen.getByText('Opening workspace...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the authenticated workspace handoff through the active locale', () => {
|
||||
setActiveLocale('es');
|
||||
|
||||
render(() => <RuntimeHome />);
|
||||
|
||||
expect(screen.getByText('Abriendo el espacio de trabajo...')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user