Add v6 RC announcement surfaces to v5

This commit is contained in:
rcourtman 2026-04-14 16:51:19 +01:00
parent dfbe2eb873
commit a24af45c67
6 changed files with 303 additions and 0 deletions

View file

@ -35,6 +35,7 @@ import { updateStore } from './stores/updates';
import { UpdateBanner } from './components/UpdateBanner';
import { DemoBanner } from './components/DemoBanner';
import { GitHubStarBanner } from './components/GitHubStarBanner';
import { ReleaseAnnouncementBanner } from './components/ReleaseAnnouncementBanner';
import { createTooltipSystem } from './components/shared/Tooltip';
import type { State, Alert } from '@/types/api';
import { ProxmoxIcon } from '@/components/icons/ProxmoxIcon';
@ -922,6 +923,10 @@ function App() {
<SecurityWarning />
<DemoBanner />
<UpdateBanner />
<ReleaseAnnouncementBanner
versionInfo={versionInfo}
securityStatus={securityStatus}
/>
<GitHubStarBanner />
<GlobalUpdateProgressWatcher />
</Show>

View file

@ -0,0 +1,115 @@
import type { Accessor } from 'solid-js';
import { createMemo, Show } from 'solid-js';
import { useLocation } from '@solidjs/router';
import type { VersionInfo } from '@/api/updates';
import type { SecurityStatus } from '@/types/config';
import {
V6_RC_ANNOUNCEMENT,
shouldShowV6RcAnnouncement,
} from '@/constants/releaseAnnouncements';
import { createLocalStorageStringSignal, STORAGE_KEYS } from '@/utils/localStorage';
import FlaskConicalIcon from 'lucide-solid/icons/flask-conical';
import ExternalLinkIcon from 'lucide-solid/icons/external-link';
import XIcon from 'lucide-solid/icons/x';
interface ReleaseAnnouncementBannerProps {
versionInfo: Accessor<VersionInfo | null>;
securityStatus: Accessor<SecurityStatus | null>;
}
export function ReleaseAnnouncementBanner(props: ReleaseAnnouncementBannerProps) {
const location = useLocation();
const [dismissedAnnouncementId, setDismissedAnnouncementId] =
createLocalStorageStringSignal(STORAGE_KEYS.RELEASE_ANNOUNCEMENT_DISMISSED, '');
const isDismissed = createMemo(
() => dismissedAnnouncementId() === V6_RC_ANNOUNCEMENT.id,
);
const shouldShow = createMemo(() => {
if (isDismissed()) {
return false;
}
return shouldShowV6RcAnnouncement({
version: props.versionInfo()?.version,
pathname: location.pathname,
securityStatus: props.securityStatus(),
});
});
const dismiss = () => {
setDismissedAnnouncementId(V6_RC_ANNOUNCEMENT.id);
};
return (
<Show when={shouldShow()}>
<div class="border-b border-emerald-200 bg-emerald-50 text-emerald-950 dark:border-emerald-900/70 dark:bg-emerald-950/40 dark:text-emerald-100">
<div class="px-4 py-3">
<div class="flex items-start justify-between gap-3">
<div class="flex min-w-0 flex-1 items-start gap-3">
<div class="mt-0.5 rounded-full bg-emerald-100 p-2 text-emerald-700 dark:bg-emerald-900/70 dark:text-emerald-200">
<FlaskConicalIcon class="h-4 w-4" />
</div>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold">Pulse v6 RC testing</span>
<span class="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] font-medium text-emerald-800 dark:bg-emerald-900/70 dark:text-emerald-200">
{V6_RC_ANNOUNCEMENT.tag}
</span>
</div>
<p class="mt-1 text-sm leading-relaxed text-emerald-900/85 dark:text-emerald-100/85">
<code class="rounded bg-emerald-100 px-1 py-0.5 text-[12px] dark:bg-emerald-900/70">
5.1.x
</code>{' '}
remains the current stable line. Pulse v6 changes the runtime,
upgrade path, navigation, and product model substantially. If you rely on
Pulse today, test v6 in a staging or non-production environment and report
any issues before the stable cut.
</p>
<div class="mt-3 flex flex-wrap items-center gap-2">
<a
href={V6_RC_ANNOUNCEMENT.changelogUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-md bg-emerald-700 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-emerald-800 dark:bg-emerald-600 dark:hover:bg-emerald-500"
>
Read v6 changelog
<ExternalLinkIcon class="h-3.5 w-3.5" />
</a>
<a
href={V6_RC_ANNOUNCEMENT.releaseUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-md border border-emerald-300 bg-white px-3 py-1.5 text-sm font-medium text-emerald-900 transition-colors hover:bg-emerald-100 dark:border-emerald-800 dark:bg-emerald-950/20 dark:text-emerald-100 dark:hover:bg-emerald-900/40"
>
View v6 RC
<ExternalLinkIcon class="h-3.5 w-3.5" />
</a>
<a
href={V6_RC_ANNOUNCEMENT.demoUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-md border border-transparent px-2 py-1.5 text-sm font-medium text-emerald-800 transition-colors hover:bg-emerald-100 dark:text-emerald-200 dark:hover:bg-emerald-900/30"
>
Open demo
<ExternalLinkIcon class="h-3.5 w-3.5" />
</a>
</div>
</div>
</div>
<button
type="button"
onClick={dismiss}
class="rounded-md p-1 text-emerald-700 transition-colors hover:bg-emerald-100 hover:text-emerald-900 dark:text-emerald-300 dark:hover:bg-emerald-900/50 dark:hover:text-emerald-100"
title="Dismiss"
aria-label="Dismiss v6 RC announcement"
>
<XIcon class="h-4 w-4" />
</button>
</div>
</div>
</div>
</Show>
);
}

View file

@ -7,6 +7,10 @@ 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 {
V6_RC_ANNOUNCEMENT,
isV5ReleaseLine,
} from '@/constants/releaseAnnouncements';
interface UpdatesSettingsPanelProps {
versionInfo: Accessor<VersionInfo | null>;
@ -39,6 +43,60 @@ export const UpdatesSettingsPanel: Component<UpdatesSettingsPanelProps> = (props
>
<section class="space-y-4">
<div class="space-y-4">
<Show when={isV5ReleaseLine(props.versionInfo()?.version)}>
<div class="rounded-xl border border-emerald-200 bg-emerald-50 p-4 dark:border-emerald-900/70 dark:bg-emerald-950/30">
<div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<div class="space-y-1">
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm font-semibold text-emerald-900 dark:text-emerald-100">
Pulse v6 RC testing
</span>
<span class="inline-flex items-center rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-800 dark:bg-emerald-900/70 dark:text-emerald-200">
{V6_RC_ANNOUNCEMENT.tag}
</span>
</div>
<p class="text-sm text-emerald-900/85 dark:text-emerald-100/85">
<code class="rounded bg-emerald-100 px-1 py-0.5 text-[12px] dark:bg-emerald-900/70">
5.1.x
</code>{' '}
remains the stable line. If you can, test v6 in a staging or
non-production environment and use the changelog before upgrading so the
move from v5 is deliberate rather than guesswork.
</p>
</div>
<div class="flex flex-wrap items-center gap-2">
<a
href={V6_RC_ANNOUNCEMENT.changelogUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-lg bg-emerald-700 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-800 dark:bg-emerald-600 dark:hover:bg-emerald-500"
>
Read v6 changelog
<Download class="w-4 h-4" />
</a>
<a
href={V6_RC_ANNOUNCEMENT.releaseUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-lg border border-emerald-300 px-3 py-2 text-sm font-medium text-emerald-900 transition-colors hover:bg-emerald-100 dark:border-emerald-800 dark:text-emerald-100 dark:hover:bg-emerald-900/40"
>
View v6 RC
<ArrowRight class="w-4 h-4" />
</a>
<a
href={V6_RC_ANNOUNCEMENT.demoUrl}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded-lg px-3 py-2 text-sm font-medium text-emerald-800 transition-colors hover:bg-emerald-100 dark:text-emerald-200 dark:hover:bg-emerald-900/30"
>
Open demo
<ArrowRight class="w-4 h-4" />
</a>
</div>
</div>
</div>
</Show>
{/* Version Status Section */}
<div class="rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
{/* Version Grid */}

View file

@ -0,0 +1,64 @@
import { describe, expect, it } from 'vitest';
import type { SecurityStatus } from '@/types/config';
import {
canSeeAdminReleaseAnnouncement,
isV5ReleaseLine,
shouldShowV6RcAnnouncement,
} from '@/constants/releaseAnnouncements';
describe('releaseAnnouncements', () => {
it('detects the v5 release line from semver strings', () => {
expect(isV5ReleaseLine('v5.1.27')).toBe(true);
expect(isV5ReleaseLine('5.1.28')).toBe(true);
expect(isV5ReleaseLine('v6.0.0-rc.1')).toBe(false);
expect(isV5ReleaseLine('')).toBe(false);
});
it('allows the announcement for non-proxy-auth sessions', () => {
const status = {
hasProxyAuth: false,
proxyAuthIsAdmin: false,
} as SecurityStatus;
expect(canSeeAdminReleaseAnnouncement(status)).toBe(true);
});
it('blocks the announcement for non-admin proxy-auth sessions', () => {
const status = {
hasProxyAuth: true,
proxyAuthIsAdmin: false,
} as SecurityStatus;
expect(canSeeAdminReleaseAnnouncement(status)).toBe(false);
});
it('shows the announcement only on the supported v5 surfaces', () => {
expect(
shouldShowV6RcAnnouncement({
version: 'v5.1.27',
pathname: '/proxmox/overview',
}),
).toBe(true);
expect(
shouldShowV6RcAnnouncement({
version: 'v5.1.27',
pathname: '/settings/updates',
}),
).toBe(true);
expect(
shouldShowV6RcAnnouncement({
version: 'v5.1.27',
pathname: '/alerts/overview',
}),
).toBe(false);
expect(
shouldShowV6RcAnnouncement({
version: 'v6.0.0-rc.1',
pathname: '/settings/updates',
}),
).toBe(false);
});
});

View file

@ -0,0 +1,60 @@
import type { SecurityStatus } from '@/types/config';
export const V6_RC_ANNOUNCEMENT = {
id: 'v6-rc-testing-v6.0.0-rc.1',
tag: 'v6.0.0-rc.1',
releaseUrl: 'https://github.com/rcourtman/Pulse/releases/tag/v6.0.0-rc.1',
changelogUrl: 'https://github.com/rcourtman/Pulse/blob/pulse/v6-release/docs/releases/V6_CHANGELOG.md',
demoUrl: 'https://v6-demo.pulserelay.pro',
} as const;
function parseMajorVersion(version?: string | null): number | null {
const match = String(version || '').trim().match(/^v?(\d+)/i);
if (!match) {
return null;
}
const major = Number(match[1]);
return Number.isFinite(major) ? major : null;
}
export function isV5ReleaseLine(version?: string | null): boolean {
return parseMajorVersion(version) === 5;
}
export function canSeeAdminReleaseAnnouncement(
securityStatus?: SecurityStatus | null,
): boolean {
if (!securityStatus) {
return true;
}
if (!securityStatus.hasProxyAuth) {
return true;
}
return securityStatus.proxyAuthIsAdmin === true;
}
export function shouldShowV6RcAnnouncement(opts: {
version?: string | null;
pathname?: string | null;
securityStatus?: SecurityStatus | null;
}): boolean {
if (!isV5ReleaseLine(opts.version)) {
return false;
}
if (!canSeeAdminReleaseAnnouncement(opts.securityStatus)) {
return false;
}
const path = opts.pathname || '';
return (
path === '/' ||
path === '/proxmox' ||
path === '/proxmox/overview' ||
path === '/settings' ||
path.startsWith('/settings/')
);
}

View file

@ -164,6 +164,7 @@ export const STORAGE_KEYS = {
// Feature discovery
DISMISSED_FEATURE_TIPS: 'pulse-dismissed-feature-tips',
RELEASE_ANNOUNCEMENT_DISMISSED: 'pulse-release-announcement-dismissed',
// GitHub star prompt
GITHUB_STAR_DISMISSED: 'pulse-github-star-dismissed',