Extract updates settings install owners

This commit is contained in:
rcourtman
2026-03-20 21:27:18 +00:00
parent 1a5f51419f
commit 63d6608846
8 changed files with 505 additions and 544 deletions
@@ -43,17 +43,20 @@ work extends shared components instead of creating new local variants.
21. `frontend-modern/src/components/Settings/SSOProvidersPanel.tsx`
22. `frontend-modern/src/components/Settings/useSSOProvidersState.ts`
23. `frontend-modern/src/components/Settings/ssoProvidersModel.ts`
24. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx`
25. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
26. `tests/integration/tests/15-settings-shell-consistency.spec.ts`
27. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts`
28. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts`
29. `frontend-modern/src/features/`
30. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
31. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
32. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
33. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
34. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
24. `frontend-modern/src/components/Settings/CopyCommandBlock.tsx`
25. `frontend-modern/src/components/Settings/UpdateInstallGuide.tsx`
26. `frontend-modern/src/components/Settings/updatesSettingsModel.ts`
27. `frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx`
28. `frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts`
29. `tests/integration/tests/15-settings-shell-consistency.spec.ts`
30. `frontend-modern/src/components/shared/PageControls.guardrails.test.ts`
31. `frontend-modern/src/components/shared/TypeColumn.guardrails.test.ts`
32. `frontend-modern/src/features/`
33. `frontend-modern/src/components/SetupWizard/SetupWizard.tsx`
34. `frontend-modern/src/components/SetupWizard/SetupCompletionPreview.tsx`
35. `frontend-modern/src/components/SetupWizard/__tests__/SetupWizard.test.tsx`
36. `frontend-modern/src/components/SetupWizard/__tests__/SetupCompletionPreview.test.tsx`
37. `frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx`
## Shared Boundaries
@@ -123,6 +126,16 @@ license/paywall lifecycle, persisted filters, verification flow, and audit-log
fetch orchestration. The shell must not re-accumulate localStorage or API
runtime logic inline.
The updates settings surface now follows the same presentation-owner rule.
`frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` stays the
top-level settings shell, while
`frontend-modern/src/components/Settings/UpdateInstallGuide.tsx`,
`frontend-modern/src/components/Settings/CopyCommandBlock.tsx`, and
`frontend-modern/src/components/Settings/updatesSettingsModel.ts` own the
deployment-specific install guide, copy-command block, and update-channel/install
model data. The panel shell must not rebuild copy-to-clipboard command cards or
deployment instruction trees inline.
General settings segmented selectors for theme preference and temperature unit
must now also route through the shared `FilterButtonGroup` primitive instead of
maintaining local button-group styling forks inside
@@ -1647,6 +1647,7 @@
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
"frontend-modern/src/components/Settings/CopyCommandBlock.tsx",
"frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx",
"frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx",
"frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx",
@@ -1658,6 +1659,8 @@
"frontend-modern/src/components/Settings/settingsPanelRegistry.ts",
"frontend-modern/src/components/Settings/ssoProvidersModel.ts",
"frontend-modern/src/components/Settings/SSOProvidersPanel.tsx",
"frontend-modern/src/components/Settings/UpdateInstallGuide.tsx",
"frontend-modern/src/components/Settings/updatesSettingsModel.ts",
"frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx",
"frontend-modern/src/components/Settings/useAuditLogPanelState.ts",
"frontend-modern/src/components/Settings/useSSOProvidersState.ts",
@@ -1705,6 +1708,7 @@
"frontend-modern/src/components/Settings/APIAccessPanel.tsx",
"frontend-modern/src/components/Settings/AuditLogPanel.tsx",
"frontend-modern/src/components/Settings/AuditWebhookPanel.tsx",
"frontend-modern/src/components/Settings/CopyCommandBlock.tsx",
"frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx",
"frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx",
"frontend-modern/src/components/Settings/RecoverySettingsPanel.tsx",
@@ -1716,6 +1720,8 @@
"frontend-modern/src/components/Settings/settingsPanelRegistry.ts",
"frontend-modern/src/components/Settings/ssoProvidersModel.ts",
"frontend-modern/src/components/Settings/SSOProvidersPanel.tsx",
"frontend-modern/src/components/Settings/UpdateInstallGuide.tsx",
"frontend-modern/src/components/Settings/updatesSettingsModel.ts",
"frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx",
"frontend-modern/src/components/Settings/useAuditLogPanelState.ts",
"frontend-modern/src/components/Settings/useSSOProvidersState.ts"
@@ -0,0 +1,42 @@
import Copy from 'lucide-solid/icons/copy';
interface CopyCommandBlockProps {
command: string;
onCopy?: (command: string) => void | Promise<void>;
containerClass?: string;
codeClass?: string;
buttonClass?: string;
}
const DEFAULT_CONTAINER_CLASS = 'relative group';
const DEFAULT_CODE_CLASS =
'block rounded-md border border-border bg-base p-3 font-mono text-sm text-base-content';
const DEFAULT_BUTTON_CLASS =
'absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100';
export function CopyCommandBlock(props: CopyCommandBlockProps) {
const handleCopy = () => {
if (props.onCopy) {
void props.onCopy(props.command);
return;
}
void navigator.clipboard.writeText(props.command);
};
return (
<div class={props.containerClass ?? DEFAULT_CONTAINER_CLASS}>
<code class={props.codeClass ?? DEFAULT_CODE_CLASS}>{props.command}</code>
<button
type="button"
onClick={handleCopy}
class={props.buttonClass ?? DEFAULT_BUTTON_CLASS}
title="Copy to clipboard"
aria-label="Copy to clipboard"
>
<Copy class="h-4 w-4" />
</button>
</div>
);
}
export default CopyCommandBlock;
@@ -0,0 +1,204 @@
import { Component, For, Show } from 'solid-js';
import Download from 'lucide-solid/icons/download';
import type { UpdateInfo, UpdatePlan, VersionInfo } from '@/api/updates';
import { CopyCommandBlock } from '@/components/Settings/CopyCommandBlock';
import {
buildIdleDockerComposeCommand,
buildIdleDockerUpdateCommand,
buildUpdateInstallGuide,
type UpdateInstallStep,
} from '@/components/Settings/updatesSettingsModel';
interface UpdateInstallGuideProps {
versionInfo: VersionInfo | null;
updateInfo: UpdateInfo | null;
updatePlan: UpdatePlan | null;
isInstalling: boolean;
dockerImageTag: string;
systemdDownloadCommand: string;
onInstallUpdate: () => void;
}
function StepIndex(props: { index: number }) {
return (
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-green-200 text-xs font-bold text-green-700 dark:bg-green-800 dark:text-green-300">
{props.index}
</span>
);
}
function InstallStep(props: { step: UpdateInstallStep; index: number }) {
return (
<div class="space-y-3">
<Show when={props.step.title}>
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<StepIndex index={props.index} />
{props.step.title}
</div>
</Show>
<Show when={props.step.command}>
<CopyCommandBlock
command={props.step.command!}
containerClass="ml-0 sm:ml-8 relative group"
codeClass={
props.step.commandCodeClass ??
'block rounded-md border border-border bg-base p-3 font-mono text-sm text-green-400'
}
/>
</Show>
<Show when={props.step.note}>
<p class="ml-0 text-xs text-green-600 dark:text-green-400 sm:ml-8">{props.step.note}</p>
</Show>
</div>
);
}
export const UpdateInstallGuide: Component<UpdateInstallGuideProps> = (props) => {
const guide = () =>
buildUpdateInstallGuide(
props.versionInfo,
props.updateInfo,
props.updatePlan,
props.dockerImageTag,
props.systemdDownloadCommand,
);
return (
<>
<Show when={props.versionInfo?.isDocker && !props.updateInfo?.available}>
<div class="space-y-3 rounded-md border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-900">
<div class="flex items-center gap-2">
<svg
class="h-5 w-5 flex-shrink-0 text-blue-600 dark:text-blue-400"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.186.186 0 00-.185.186v1.887c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.186.186 0 00-.185.185v1.888c0 .102.082.186.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.186.186 0 00-.185.185v1.888c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.186.186 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.888c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.186.186 0 00-.185.186v1.887c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.186v1.887c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.119a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-.001 2.716h2.118a.185.185 0 00.185-.185v-1.888a.185.185 0 00-.185-.185H2.136a.185.185 0 00-.186.185v1.888c0 .102.084.185.186.185m23.063-3.167a.509.509 0 00-.376-.25.431.431 0 00-.116-.01.431.431 0 00-.114.01 3.6 3.6 0 00-1.618.877c-.186.166-.356.36-.509.577a6.6 6.6 0 00-1.117-1.474 6.6 6.6 0 00-9.336 0 6.6 6.6 0 00-1.938 4.684 6.6 6.6 0 001.938 4.684 6.6 6.6 0 004.668 1.938 6.6 6.6 0 004.668-1.938 6.6 6.6 0 001.938-4.684 6.6 6.6 0 00-.185-1.41 3.6 3.6 0 001.587-.904.509.509 0 00.134-.459" />
</svg>
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">Docker Installation</p>
</div>
<p class="text-xs text-blue-700 dark:text-blue-300">
Updates are managed through Docker. Use these commands to check for and apply updates:
</p>
<div class="space-y-2">
<CopyCommandBlock
command={buildIdleDockerUpdateCommand()}
codeClass="block rounded-md border border-border bg-base p-2.5 font-mono text-xs text-blue-400"
/>
<p class="text-[10px] text-blue-600 dark:text-blue-400">
Or with Docker Compose:{' '}
<code class="rounded bg-blue-100 px-1 py-0.5 text-[10px] dark:bg-blue-800">
{buildIdleDockerComposeCommand()}
</code>
</p>
</div>
</div>
</Show>
<Show when={props.versionInfo?.isSourceBuild}>
<div class="rounded-md border border-blue-200 bg-blue-50 p-3 dark:border-blue-800 dark:bg-blue-900">
<p class="text-xs text-blue-800 dark:text-blue-200">
<strong>Built from source:</strong> Pull the latest code from git and rebuild to
update.
</p>
</div>
</Show>
<Show when={props.updateInfo?.warning}>
<div class="rounded-md border border-amber-200 bg-amber-50 p-3 dark:border-amber-700 dark:bg-amber-900">
<p class="text-xs text-amber-800 dark:text-amber-200">{props.updateInfo?.warning}</p>
</div>
</Show>
<Show when={props.updateInfo?.available && guide()}>
<div class="overflow-hidden rounded-md border border-green-200 bg-green-50 dark:border-green-700 dark:bg-green-900">
<div class="border-b border-green-200 bg-green-100 px-5 py-4 dark:border-green-800 dark:bg-green-800">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3">
<div class="rounded-md bg-green-100 p-2 dark:bg-green-900">
<svg
class="h-5 w-5 text-green-700 dark:text-green-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
</svg>
</div>
<div>
<h4 class="text-base font-semibold text-green-900 dark:text-green-100">
{guide()!.headerTitle}
</h4>
<p class="text-xs text-green-700 dark:text-green-300">{guide()!.headerSummary}</p>
</div>
</div>
<Show when={props.updatePlan?.canAutoUpdate}>
<button
type="button"
onClick={props.onInstallUpdate}
disabled={props.isInstalling}
class={`flex w-full items-center justify-center gap-2 rounded-md px-4 py-2.5 text-sm font-medium transition-all sm:w-auto ${
props.isInstalling
? 'cursor-not-allowed bg-green-400 text-white dark:bg-green-600'
: 'bg-green-600 text-white hover:bg-green-700'
}`}
>
<Show
when={props.isInstalling}
fallback={
<>
<Download class="h-4 w-4" />
Install Update
</>
}
>
<div class="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
Installing...
</Show>
</button>
</Show>
</div>
</div>
<div class="space-y-4 p-5">
<div class="mb-3 text-sm text-green-700 dark:text-green-300">{guide()!.introText}</div>
<For each={guide()!.steps}>
{(step, index) => <InstallStep step={step} index={index() + 1} />}
</For>
</div>
<Show when={props.updateInfo?.releaseNotes}>
<div class="border-t border-green-200 bg-surface px-5 py-3 dark:border-green-800">
<details class="group">
<summary class="flex cursor-pointer items-center gap-2 text-sm font-medium text-green-700 transition-colors hover:text-green-800 dark:text-green-300 dark:hover:text-green-200">
<svg
class="h-4 w-4 transition-transform group-open:rotate-90"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
View Release Notes
</summary>
<pre class="mt-3 max-h-64 overflow-y-auto rounded-md border border-border bg-surface-alt p-4 font-mono text-xs whitespace-pre-wrap text-base-content">
{props.updateInfo?.releaseNotes}
</pre>
</details>
</div>
</Show>
</div>
</Show>
</>
);
};
export default UpdateInstallGuide;
@@ -9,9 +9,13 @@ import RefreshCw from 'lucide-solid/icons/refresh-cw';
import CheckCircle from 'lucide-solid/icons/check-circle';
import ArrowRight from 'lucide-solid/icons/arrow-right';
import Package from 'lucide-solid/icons/package';
import Download from 'lucide-solid/icons/download';
import type { UpdateInfo, VersionInfo, UpdatePlan } from '@/api/updates';
import { buildDockerImageTag, buildLinuxAmd64DownloadCommand } from '@/components/updateVersion';
import { UpdateInstallGuide } from '@/components/Settings/UpdateInstallGuide';
import {
getUpdateChannelCardOptions,
type UpdateChannelOptionValue,
} from '@/components/Settings/updatesSettingsModel';
import {
getUpdateAvailabilityHeading,
getUpdateBuildBadges,
@@ -45,52 +49,40 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
const systemdDownloadCommand = () => buildLinuxAmd64DownloadCommand(latestVersion());
const isPreviewChannel = () => props.updateChannel() === 'rc';
const autoUpdateLocked = () => Boolean(props.versionInfo()?.isSourceBuild || isPreviewChannel());
const updateChannelOptions = (): SelectionCardOption<'stable' | 'rc'>[] => [
{
value: 'stable',
title: 'Stable',
description: 'Production-ready releases for paid and self-hosted environments',
tone: 'success',
disabled: props.versionInfo()?.isSourceBuild,
icon: ({ active }) => (
<svg
class={`w-5 h-5 ${active ? 'text-green-600 dark:text-green-400' : 'text-muted'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
),
},
{
value: 'rc',
title: 'Release Candidate',
description: 'Preview builds for staging, internal validation, and opt-in testers',
tone: 'accent',
disabled: props.versionInfo()?.isSourceBuild,
icon: ({ active }) => (
<svg
class={`w-5 h-5 ${active ? 'text-blue-600 dark:text-blue-400' : 'text-muted'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
/>
</svg>
),
},
];
const updateChannelOptions = (): SelectionCardOption<UpdateChannelOptionValue>[] =>
getUpdateChannelCardOptions(props.versionInfo()).map((option) => ({
...option,
icon: (iconProps) =>
option.value === 'stable' ? (
<svg
class={`w-5 h-5 ${iconProps.active ? 'text-green-600 dark:text-green-400' : 'text-muted'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"
/>
</svg>
) : (
<svg
class={`w-5 h-5 ${iconProps.active ? 'text-blue-600 dark:text-blue-400' : 'text-muted'}`}
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.428 15.428a2 2 0 00-1.022-.547l-2.387-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"
/>
</svg>
),
}));
return (
<SettingsPanel
@@ -222,492 +214,15 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
</div>
</div>
{/* Docker installation notice - enhanced with copy-able commands */}
<Show when={props.versionInfo()?.isDocker && !props.updateInfo()?.available}>
<div class="p-4 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-800 rounded-md space-y-3">
<div class="flex items-center gap-2">
<svg
class="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0"
viewBox="0 0 24 24"
fill="currentColor"
>
<path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.186.186 0 00-.185.186v1.887c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.186.186 0 00-.185.185v1.888c0 .102.082.186.185.186m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.186.186 0 00-.185.185v1.888c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.186.186 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.888c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.186.186 0 00-.185.186v1.887c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.186v1.887c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.185-.186h-2.119a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-.001 2.716h2.118a.185.185 0 00.185-.185v-1.888a.185.185 0 00-.185-.185H2.136a.185.185 0 00-.186.185v1.888c0 .102.084.185.186.185m23.063-3.167a.509.509 0 00-.376-.25.431.431 0 00-.116-.01.431.431 0 00-.114.01 3.6 3.6 0 00-1.618.877c-.186.166-.356.36-.509.577a6.6 6.6 0 00-1.117-1.474 6.6 6.6 0 00-9.336 0 6.6 6.6 0 00-1.938 4.684 6.6 6.6 0 001.938 4.684 6.6 6.6 0 004.668 1.938 6.6 6.6 0 004.668-1.938 6.6 6.6 0 001.938-4.684 6.6 6.6 0 00-.185-1.41 3.6 3.6 0 001.587-.904.509.509 0 00.134-.459" />
</svg>
<p class="text-sm font-medium text-blue-800 dark:text-blue-200">
Docker Installation
</p>
</div>
<p class="text-xs text-blue-700 dark:text-blue-300">
Updates are managed through Docker. Use these commands to check for and apply
updates:
</p>
<div class="space-y-2">
<div class="relative group">
<code class="block p-2.5 bg-base rounded-md text-xs font-mono text-blue-400 border border-border">
docker pull rcourtman/pulse:latest && docker restart pulse
</code>
<button
type="button"
onClick={() =>
navigator.clipboard.writeText(
'docker pull rcourtman/pulse:latest && docker restart pulse',
)
}
class="absolute top-1.5 right-1.5 p-1 rounded bg-surface-hover hover:bg-slate-600 text-slate-300 opacity-60 hover:opacity-100 transition-opacity"
title="Copy to clipboard"
>
<svg
class="w-3.5 h-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
<p class="text-[10px] text-blue-600 dark:text-blue-400">
Or with Docker Compose:{' '}
<code class="px-1 py-0.5 bg-blue-100 dark:bg-blue-800 rounded text-[10px]">
docker-compose pull && docker-compose up -d
</code>
</p>
</div>
</div>
</Show>
{/* Source build notice */}
<Show when={props.versionInfo()?.isSourceBuild}>
<div class="p-3 bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-800 rounded-md">
<p class="text-xs text-blue-800 dark:text-blue-200">
<strong>Built from source:</strong> Pull the latest code from git and rebuild to
update.
</p>
</div>
</Show>
{/* Warning message */}
<Show when={Boolean(props.updateInfo()?.warning)}>
<div class="p-3 bg-amber-50 dark:bg-amber-900 border border-amber-200 dark:border-amber-700 rounded-md">
<p class="text-xs text-amber-800 dark:text-amber-200">
{props.updateInfo()?.warning}
</p>
</div>
</Show>
{/* Update available */}
<Show when={props.updateInfo()?.available}>
<div class="rounded-md border border-green-200 dark:border-green-700 overflow-hidden bg-green-50 dark:bg-green-900">
{/* Header */}
<div class="px-5 py-4 border-b border-green-200 dark:border-green-800 bg-green-100 dark:bg-green-800">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex items-center gap-3">
<div class="p-2 bg-green-100 dark:bg-green-900 rounded-md">
<svg
class="w-5 h-5 text-green-700 dark:text-green-300"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"
/>
</svg>
</div>
<div>
<h4 class="text-base font-semibold text-green-900 dark:text-green-100">
Update Available
</h4>
<p class="text-xs text-green-700 dark:text-green-300">
Version {props.updateInfo()?.latestVersion} is ready to install
</p>
</div>
</div>
{/* Automated Install Button */}
<Show when={props.updatePlan()?.canAutoUpdate}>
<button
type="button"
onClick={props.onInstallUpdate}
disabled={props.isInstalling()}
class={`w-full justify-center sm:w-auto px-4 py-2.5 rounded-md text-sm font-medium transition-all flex items-center gap-2 ${
props.isInstalling()
? 'bg-green-400 dark:bg-green-600 text-white cursor-not-allowed'
: 'bg-green-600 hover:bg-green-700 text-white'
}`}
>
<Show
when={props.isInstalling()}
fallback={
<>
<Download class="w-4 h-4" />
Install Update
</>
}
>
<div class="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></div>
Installing...
</Show>
</button>
</Show>
</div>
</div>
{/* Installation Steps */}
<div class="p-5 space-y-4">
{/* Manual Steps Header */}
<Show when={!props.updatePlan()?.canAutoUpdate}>
<div class="text-sm font-medium text-green-800 dark:text-green-200 mb-3">
Follow these steps to update manually:
</div>
</Show>
<Show when={props.updatePlan()?.canAutoUpdate}>
<div class="text-sm text-green-700 dark:text-green-300 mb-3">
Click "Install Update" above for automatic installation, or update manually:
</div>
</Show>
{/* ProxmoxVE LXC Installation */}
<Show when={props.versionInfo()?.deploymentType === 'proxmoxve'}>
<div class="space-y-3">
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
1
</span>
Open your Pulse LXC console
</div>
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
2
</span>
Run the update command:
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border">
update
</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText('update')}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
<p class="ml-0 sm:ml-8 text-xs text-green-600 dark:text-green-400">
The script will automatically download and install the latest version.
</p>
</div>
</Show>
{/* Docker Installation */}
<Show
when={
props.versionInfo()?.deploymentType === 'docker' ||
(!props.versionInfo()?.deploymentType && props.versionInfo()?.isDocker)
}
>
<div class="space-y-3">
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
1
</span>
Pull the latest image
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border">
docker pull rcourtman/pulse:{dockerImageTag()}
</code>
<button
type="button"
onClick={() =>
navigator.clipboard.writeText(
`docker pull rcourtman/pulse:${dockerImageTag()}`,
)
}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
2
</span>
Restart the container
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border">
docker restart pulse
</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText('docker restart pulse')}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
<p class="ml-0 sm:ml-8 text-xs text-green-600 dark:text-green-400">
Or use Docker Compose:{' '}
<code class="px-1.5 py-0.5 bg-surface-hover rounded text-xs">
docker-compose pull && docker-compose up -d
</code>
</p>
</div>
</Show>
{/* Systemd/Manual Installation */}
<Show
when={
props.versionInfo()?.deploymentType === 'systemd' ||
props.versionInfo()?.deploymentType === 'manual'
}
>
<div class="space-y-3">
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
1
</span>
Stop the service
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border">
sudo systemctl stop pulse
</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText('sudo systemctl stop pulse')}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
2
</span>
Download and extract the new version
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border whitespace-pre-wrap break-all">
{systemdDownloadCommand()}
</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText(systemdDownloadCommand())}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
3
</span>
Start the service
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border">
sudo systemctl start pulse
</code>
<button
type="button"
onClick={() =>
navigator.clipboard.writeText('sudo systemctl start pulse')
}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
</div>
</Show>
{/* Development Installation */}
<Show when={props.versionInfo()?.deploymentType === 'development'}>
<div class="space-y-3">
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
1
</span>
Pull the latest changes
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border">
git pull origin main
</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText('git pull origin main')}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
<div class="flex items-center gap-2 text-sm font-medium text-green-800 dark:text-green-200">
<span class="flex items-center justify-center w-6 h-6 rounded-full bg-green-200 dark:bg-green-800 text-xs font-bold text-green-700 dark:text-green-300">
2
</span>
Rebuild and restart
</div>
<div class="ml-0 sm:ml-8 relative group">
<code class="block p-3 bg-base rounded-md text-sm font-mono text-green-400 border border-border">
make build && make run
</code>
<button
type="button"
onClick={() => navigator.clipboard.writeText('make build && make run')}
class="absolute right-2 top-2 inline-flex min-h-9 min-w-9 items-center justify-center rounded bg-surface-hover p-2 text-slate-300 opacity-70 transition-opacity hover:bg-slate-600 hover:opacity-100"
title="Copy to clipboard"
>
<svg
class="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
/>
</svg>
</button>
</div>
</div>
</Show>
</div>
{/* Release notes footer */}
<Show when={props.updateInfo()?.releaseNotes}>
<div class="px-5 py-3 border-t border-green-200 dark:border-green-800 bg-surface">
<details class="group">
<summary class="flex items-center gap-2 text-sm font-medium text-green-700 dark:text-green-300 cursor-pointer hover:text-green-800 dark:hover:text-green-200 transition-colors">
<svg
class="w-4 h-4 transition-transform group-open:rotate-90"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width="2"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
</svg>
View Release Notes
</summary>
<pre class="mt-3 p-4 text-xs text-base-content whitespace-pre-wrap font-mono bg-surface-alt rounded-md border border-border max-h-64 overflow-y-auto">
{props.updateInfo()?.releaseNotes}
</pre>
</details>
</div>
</Show>
</div>
</Show>
<UpdateInstallGuide
versionInfo={props.versionInfo()}
updateInfo={props.updateInfo()}
updatePlan={props.updatePlan()}
isInstalling={props.isInstalling()}
dockerImageTag={dockerImageTag()}
systemdDownloadCommand={systemdDownloadCommand()}
onInstallUpdate={props.onInstallUpdate}
/>
</div>
</div>
@@ -21,6 +21,9 @@ import aiProviderConfigurationSectionSource from '../AIProviderConfigurationSect
import aiSettingsDialogsSource from '../AISettingsDialogs.tsx?raw';
import aiSettingsModelSource from '../aiSettingsModel.ts?raw';
import networkSettingsPanelSource from '../NetworkSettingsPanel.tsx?raw';
import copyCommandBlockSource from '../CopyCommandBlock.tsx?raw';
import updateInstallGuideSource from '../UpdateInstallGuide.tsx?raw';
import updatesSettingsModelSource from '../updatesSettingsModel.ts?raw';
import updatesSettingsPanelSource from '../UpdatesSettingsPanel.tsx?raw';
import recoverySettingsPanelSource from '../RecoverySettingsPanel.tsx?raw';
import relaySettingsPanelSource from '../RelaySettingsPanel.tsx?raw';
@@ -61,6 +64,9 @@ const extractedModules = [
'../AIProviderConfigurationSection.tsx',
'../AISettingsDialogs.tsx',
'../aiSettingsModel.ts',
'../CopyCommandBlock.tsx',
'../UpdateInstallGuide.tsx',
'../updatesSettingsModel.ts',
'../useSSOProvidersState.ts',
'../ssoProvidersModel.ts',
'../ProxmoxSettingsPanel.tsx',
@@ -349,6 +355,20 @@ describe('Settings architecture guardrails', () => {
expect(aiSettingsModelSource).toContain('export const AI_SETUP_PROVIDER_OPTIONS');
});
it('keeps the updates settings shell behind extracted install-guide owners', () => {
expect(updatesSettingsPanelSource).toContain('@/components/Settings/UpdateInstallGuide');
expect(updatesSettingsPanelSource).toContain('@/components/Settings/updatesSettingsModel');
expect(updatesSettingsPanelSource).not.toContain("navigator.clipboard.writeText('update')");
expect(updatesSettingsPanelSource).not.toContain("value: 'stable'");
expect(updatesSettingsPanelSource).not.toContain("value: 'rc'");
expect(updateInstallGuideSource).toContain('@/components/Settings/CopyCommandBlock');
expect(updateInstallGuideSource).toContain('buildUpdateInstallGuide');
expect(copyCommandBlockSource).toContain('export function CopyCommandBlock');
expect(copyCommandBlockSource).toContain("aria-label=\"Copy to clipboard\"");
expect(updatesSettingsModelSource).toContain('export function getUpdateChannelCardOptions');
expect(updatesSettingsModelSource).toContain('export function buildUpdateInstallGuide');
});
it('keeps the audit log shell behind an extracted runtime owner', () => {
expect(auditLogPanelSource).toContain('@/components/Settings/useAuditLogPanelState');
expect(auditLogPanelSource).not.toContain('createLocalStorageStringSignal');
@@ -0,0 +1,149 @@
import type { UpdateInfo, UpdatePlan, VersionInfo } from '@/api/updates';
export type UpdateChannelOptionValue = 'stable' | 'rc';
export interface UpdateChannelCardOption {
value: UpdateChannelOptionValue;
title: string;
description: string;
tone: 'success' | 'accent';
disabled?: boolean;
}
export interface UpdateInstallStep {
id: string;
title: string;
command?: string;
note?: string;
commandCodeClass?: string;
}
export interface UpdateInstallGuide {
headerTitle: string;
headerSummary: string;
introText: string;
steps: UpdateInstallStep[];
}
export function getUpdateChannelCardOptions(
versionInfo?: Pick<VersionInfo, 'isSourceBuild'> | null,
): UpdateChannelCardOption[] {
return [
{
value: 'stable',
title: 'Stable',
description: 'Production-ready releases for paid and self-hosted environments',
tone: 'success',
disabled: versionInfo?.isSourceBuild,
},
{
value: 'rc',
title: 'Release Candidate',
description: 'Preview builds for staging, internal validation, and opt-in testers',
tone: 'accent',
disabled: versionInfo?.isSourceBuild,
},
];
}
export function buildIdleDockerUpdateCommand(): string {
return 'docker pull rcourtman/pulse:latest && docker restart pulse';
}
export function buildIdleDockerComposeCommand(): string {
return 'docker-compose pull && docker-compose up -d';
}
export function buildUpdateInstallGuide(
versionInfo: Pick<
VersionInfo,
'deploymentType' | 'isDocker'
> | null | undefined,
updateInfo: Pick<UpdateInfo, 'latestVersion'> | null | undefined,
updatePlan: Pick<UpdatePlan, 'canAutoUpdate'> | null | undefined,
dockerImageTag: string,
systemdDownloadCommand: string,
): UpdateInstallGuide | null {
if (!versionInfo) {
return null;
}
const introText = updatePlan?.canAutoUpdate
? 'Click "Install Update" above for automatic installation, or update manually:'
: 'Follow these steps to update manually:';
if (versionInfo.deploymentType === 'proxmoxve') {
return {
headerTitle: 'Update Available',
headerSummary: `Version ${updateInfo?.latestVersion} is ready to install`,
introText,
steps: [
{ id: 'open-console', title: 'Open your Pulse LXC console' },
{ id: 'run-update', title: 'Run the update command:', command: 'update' },
{
id: 'run-update-note',
title: '',
note: 'The script will automatically download and install the latest version.',
},
],
};
}
if (versionInfo.deploymentType === 'docker' || (!versionInfo.deploymentType && versionInfo.isDocker)) {
return {
headerTitle: 'Update Available',
headerSummary: `Version ${updateInfo?.latestVersion} is ready to install`,
introText,
steps: [
{
id: 'docker-pull',
title: 'Pull the latest image',
command: `docker pull rcourtman/pulse:${dockerImageTag}`,
},
{
id: 'docker-restart',
title: 'Restart the container',
command: 'docker restart pulse',
},
{
id: 'docker-compose-note',
title: '',
note: 'Or use Docker Compose: docker-compose pull && docker-compose up -d',
},
],
};
}
if (versionInfo.deploymentType === 'systemd' || versionInfo.deploymentType === 'manual') {
return {
headerTitle: 'Update Available',
headerSummary: `Version ${updateInfo?.latestVersion} is ready to install`,
introText,
steps: [
{ id: 'systemd-stop', title: 'Stop the service', command: 'sudo systemctl stop pulse' },
{
id: 'systemd-download',
title: 'Download and extract the new version',
command: systemdDownloadCommand,
commandCodeClass:
'block rounded-md border border-border bg-base p-3 font-mono text-sm text-base-content whitespace-pre-wrap break-all',
},
{ id: 'systemd-start', title: 'Start the service', command: 'sudo systemctl start pulse' },
],
};
}
if (versionInfo.deploymentType === 'development') {
return {
headerTitle: 'Update Available',
headerSummary: `Version ${updateInfo?.latestVersion} is ready to install`,
introText,
steps: [
{ id: 'development-pull', title: 'Pull the latest changes', command: 'git pull origin main' },
{ id: 'development-build', title: 'Rebuild and restart', command: 'make build && make run' },
],
};
}
return null;
}
@@ -19,6 +19,9 @@ import storageSourcesSource from '@/utils/storageSources.ts?raw';
import rbacPermissionsSource from '@/utils/rbacPermissions.ts?raw';
import systemSettingsPresentationSource from '@/utils/systemSettingsPresentation.ts?raw';
import updatesSettingsPanelSource from '@/components/Settings/UpdatesSettingsPanel.tsx?raw';
import copyCommandBlockSource from '@/components/Settings/CopyCommandBlock.tsx?raw';
import updateInstallGuideSource from '@/components/Settings/UpdateInstallGuide.tsx?raw';
import updatesSettingsModelSource from '@/components/Settings/updatesSettingsModel.ts?raw';
import updatesPresentationSource from '@/utils/updatesPresentation.ts?raw';
import environmentLockBadgeSource from '@/components/shared/EnvironmentLockBadge.tsx?raw';
import environmentLockPresentationSource from '@/utils/environmentLockPresentation.ts?raw';
@@ -2680,10 +2683,19 @@ describe('frontend resource type boundaries', () => {
expect(updatesSettingsPanelSource).toContain('getUpdateAvailabilityHeading');
expect(updatesSettingsPanelSource).toContain('getUpdatePrimaryStatusLabel');
expect(updatesSettingsPanelSource).toContain('getUpdateCheckModeLabel');
expect(updatesSettingsPanelSource).toContain('@/components/Settings/UpdateInstallGuide');
expect(updatesSettingsPanelSource).toContain('@/components/Settings/updatesSettingsModel');
expect(updatesSettingsPanelSource).not.toContain('Auto-check enabled');
expect(updatesSettingsPanelSource).not.toContain('Manual checks only');
expect(updatesSettingsPanelSource).not.toContain('Update Ready');
expect(updatesSettingsPanelSource).not.toContain('Up to date');
expect(updatesSettingsPanelSource).not.toContain("navigator.clipboard.writeText('update')");
expect(updateInstallGuideSource).toContain('@/components/Settings/CopyCommandBlock');
expect(updateInstallGuideSource).toContain('buildUpdateInstallGuide');
expect(copyCommandBlockSource).toContain('export function CopyCommandBlock');
expect(copyCommandBlockSource).toContain("aria-label=\"Copy to clipboard\"");
expect(updatesSettingsModelSource).toContain('export function getUpdateChannelCardOptions');
expect(updatesSettingsModelSource).toContain('export function buildUpdateInstallGuide');
expect(updatesPresentationSource).toContain('export function getUpdateBuildBadges');
expect(updatesPresentationSource).toContain('export function getUpdateAvailabilityHeading');
expect(updatesPresentationSource).toContain('export function getUpdatePrimaryStatusLabel');