mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-12 07:18:49 +00:00
Extract updates settings install owners
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user