Remove v6 RC banner from app shell

This commit is contained in:
rcourtman 2026-04-17 20:06:57 +01:00
parent 4e4fcb5dbe
commit 00da144dd8
12 changed files with 101 additions and 190 deletions

View file

@ -237,7 +237,11 @@ they must remain presentation-only. Prerelease banners, billing callouts, or
other header-adjacent notices must not fork assistant open state, gate on AI
runtime fetches, or move assistant availability logic out of
`frontend-modern/src/stores/aiChat.ts` and `frontend-modern/src/useAppRuntimeState.ts`
just because they share the same authenticated shell.
just because they share the same authenticated shell. The remaining
prerelease-shell treatment is the compact `Preview` badge on rc-channel
builds; `frontend-modern/src/AppLayout.tsx` must not revive a standalone
release-candidate banner, release-notes CTA, or feedback CTA that starts
participating in assistant-shell state or modal ownership.
That same shared shell boundary must respect blocking modal ownership.
`frontend-modern/src/App.tsx` and `frontend-modern/src/AppLayout.tsx` may use
the shared dialog runtime to hide the closed assistant launcher and close the

View file

@ -625,7 +625,11 @@ That same shell framing also owns user-facing prerelease labeling for
rc-channel builds. `frontend-modern/src/AppLayout.tsx` may still key off the
canonical `rc` channel metadata internally, but the visible badge must frame
those builds as a preview/prerelease experience rather than implying a
near-ready release candidate.
near-ready release candidate. That authenticated shell must not pair the
preview label with a second top-of-shell release-candidate warning banner or
release-feedback CTA in `frontend-modern/src/AppLayout.tsx`; prerelease cloud
posture stays a subtle shell label, not a public-RC callout inside the paid
runtime chrome.
The shared trial-start runtime is part of that same cloud-paid boundary.
Commercial relay, onboarding, setup, Pro settings, and shared paywall
surfaces may customize success copy, but they must route hosted handoff,

View file

@ -644,15 +644,15 @@ paywall surfaces navigate those destinations. Feature shells may request a
commercial destination, but they must not re-decide whether that destination
opens in-app or in a new tab once the shared primitive exists.
That same shared-primitive floor now also owns prerelease shell guidance.
`frontend-modern/src/components/shared/ReleaseCandidateBanner.tsx` is the
canonical low-key release-candidate callout for authenticated chrome, and
`frontend-modern/src/AppLayout.tsx` may mount it only from resolved release
metadata. Feature pages, settings panels, and route-local shells must not add
duplicate RC modals, hardcoded GitHub release or feedback links, or page-local
prerelease banners once that shared primitive exists. The shared banner copy
must stay version-aware but RC-order-agnostic: later builds like `rc.2` and
beyond may identify the current version, but they must not keep claiming to be
the first public v6 RC once the release line has moved on.
`frontend-modern/src/AppLayout.tsx` is the canonical authenticated-shell owner
for prerelease presentation, and the remaining user-facing treatment is the
compact `Preview` badge keyed from resolved release metadata. Feature pages,
settings panels, shared components, and route-local shells must not add a
second release-candidate banner, hardcoded GitHub release or feedback links,
or page-local prerelease notices once that shared shell contract exists.
Browser proof for that shell rule now lives in
`tests/integration/tests/57-release-candidate-shell.spec.ts`, which must keep
rc-channel builds banner-free while preserving the compact preview badge.
The subsystem registry now also requires explicit proof-policy coverage for all
shared runtime files, and shared-component guardrails fail if raw table

View file

@ -19,7 +19,6 @@ import SettingsIcon from 'lucide-solid/icons/settings';
import Maximize2Icon from 'lucide-solid/icons/maximize-2';
import Minimize2Icon from 'lucide-solid/icons/minimize-2';
import { MobileNavBar } from '@/components/shared/MobileNavBar';
import { ReleaseCandidateBanner } from '@/components/shared/ReleaseCandidateBanner';
import { dialogStackHasBlockingDialog } from '@/components/shared/useDialogState';
import { OrgSwitcher } from '@/components/OrgSwitcher';
import { PulsePatrolLogo } from '@/components/Brand/PulsePatrolLogo';
@ -646,12 +645,6 @@ export function AppLayout(props: AppLayoutProps) {
</div>
</div>
<Show when={!kioskMode()}>
<Show when={props.versionInfo()?.channel === 'rc'}>
<ReleaseCandidateBanner version={props.versionInfo()?.version} />
</Show>
</Show>
<Show when={!kioskMode()}>
<div
class="tabs mb-2 hidden md:flex items-end gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap border-b border-border scrollbar-hide"

View file

@ -70,9 +70,6 @@ describe('App architecture', () => {
expect(appLayoutSource).toContain(
"import { dialogStackHasBlockingDialog } from '@/components/shared/useDialogState';",
);
expect(appLayoutSource).toContain(
"import { ReleaseCandidateBanner } from '@/components/shared/ReleaseCandidateBanner';",
);
expect(appLayoutSource).toContain('<OrgSwitcher');
expect(appLayoutSource).toContain('const status = () => props.connectionStatus();');
expect(appLayoutSource).toContain("status().kind === 'sync-reconnecting' || status().kind === 'reconnecting'");
@ -81,7 +78,10 @@ describe('App architecture', () => {
);
expect(appLayoutSource).toContain("props.versionInfo()?.channel === 'rc'");
expect(appLayoutSource).toContain('Preview');
expect(appLayoutSource).toContain(
expect(appLayoutSource).not.toContain(
"import { ReleaseCandidateBanner } from '@/components/shared/ReleaseCandidateBanner';",
);
expect(appLayoutSource).not.toContain(
'<ReleaseCandidateBanner version={props.versionInfo()?.version} />',
);
expect(appLayoutSource).toContain(

View file

@ -1,11 +1,9 @@
import { describe, expect, it } from 'vitest';
import {
buildIssueTemplateUrl,
buildDockerImageTag,
buildLinuxAmd64DownloadCommand,
buildLinuxAmd64TarballName,
buildReleaseNotesUrl,
buildV6RcFeedbackUrl,
formatReleaseTag,
normalizeReleaseVersion,
} from '@/components/updateVersion';
@ -42,13 +40,4 @@ describe('updateVersion helpers', () => {
'sudo tar -xzf pulse-v5.1.0-linux-amd64.tar.gz -C /usr/local/bin pulse',
);
});
it('builds GitHub issue template links for prerelease feedback', () => {
expect(buildIssueTemplateUrl('v6_rc_feedback.yml')).toBe(
'https://github.com/rcourtman/Pulse/issues/new?template=v6_rc_feedback.yml',
);
expect(buildV6RcFeedbackUrl()).toBe(
'https://github.com/rcourtman/Pulse/issues/new?template=v6_rc_feedback.yml',
);
});
});

View file

@ -1,59 +0,0 @@
import type { Component } from 'solid-js';
import {
buildReleaseNotesUrl,
buildV6RcFeedbackUrl,
normalizeReleaseVersion,
} from '@/components/updateVersion';
export interface ReleaseCandidateBannerProps {
version?: string | null;
}
export const ReleaseCandidateBanner: Component<ReleaseCandidateBannerProps> = (props) => {
const versionLabel = () => normalizeReleaseVersion(props.version);
const title = () =>
versionLabel()
? `Pulse ${versionLabel()} is a public v6 release candidate.`
: 'Youre running a Pulse v6 release candidate build.';
return (
<div
class="mb-3 rounded-md border border-amber-300 bg-amber-50 px-3 py-2 text-amber-950 dark:border-amber-800 dark:bg-amber-950/40 dark:text-amber-100"
role="status"
aria-live="polite"
>
<div class="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
<div class="min-w-0">
<div class="flex flex-wrap items-center gap-2">
<span class="inline-flex rounded-full bg-amber-600 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-white">
RC
</span>
<span class="text-sm font-medium">{title()}</span>
</div>
<p class="mt-1 text-xs leading-relaxed text-amber-900/85 dark:text-amber-100/85">
Start in a staging or non-critical environment first, then send feedback on bugs,
regressions, or rough edges before general availability.
</p>
</div>
<div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs font-medium">
<a
href={buildReleaseNotesUrl(props.version)}
target="_blank"
rel="noopener noreferrer"
class="inline-flex min-h-9 items-center rounded px-1 py-1 underline underline-offset-2 hover:opacity-90"
>
View release notes
</a>
<a
href={buildV6RcFeedbackUrl()}
target="_blank"
rel="noopener noreferrer"
class="inline-flex min-h-9 items-center rounded px-1 py-1 underline underline-offset-2 hover:opacity-90"
>
Send feedback
</a>
</div>
</div>
</div>
);
};

View file

@ -72,7 +72,6 @@ import infrastructureSummaryTableModelSource from '@/components/shared/infrastru
import infrastructureSummaryTableStateSource from '@/components/shared/useInfrastructureSummaryTableState.ts?raw';
import monitoredSystemLimitWarningBannerSource from '@/components/shared/MonitoredSystemLimitWarningBanner.tsx?raw';
import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw';
import releaseCandidateBannerSource from '@/components/shared/ReleaseCandidateBanner.tsx?raw';
import selectionCardGroupSource from '@/components/shared/SelectionCardGroup.tsx?raw';
import selectionCardGroupModelSource from '@/components/shared/selectionCardGroupModel.ts?raw';
import summaryMetricCardSource from '@/components/shared/SummaryMetricCard.tsx?raw';
@ -692,19 +691,6 @@ describe('shared primitive guardrails', () => {
);
});
it('keeps release-candidate shell guidance on shared link helpers', () => {
expect(releaseCandidateBannerSource).toContain('buildReleaseNotesUrl');
expect(releaseCandidateBannerSource).toContain('buildV6RcFeedbackUrl');
expect(releaseCandidateBannerSource).toContain('normalizeReleaseVersion');
expect(releaseCandidateBannerSource).toContain('public v6 release candidate');
expect(releaseCandidateBannerSource).not.toContain('first public v6 RC');
expect(releaseCandidateBannerSource).not.toContain('useNavigate');
expect(releaseCandidateBannerSource).not.toContain('createSignal');
expect(releaseCandidateBannerSource).not.toContain('loadCommercialPosture');
expect(releaseCandidateBannerSource).not.toContain('/api/license/');
expect(releaseCandidateBannerSource).not.toContain('window.location');
});
it('keeps infrastructure summary table on shell, runtime, and model owners', () => {
expect(infrastructureSummaryTableSource).toContain('useInfrastructureSummaryTableState');
expect(infrastructureSummaryTableSource).toContain('InfrastructureSummaryTableRow');

View file

@ -1,30 +0,0 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it } from 'vitest';
import { ReleaseCandidateBanner } from '@/components/shared/ReleaseCandidateBanner';
afterEach(() => {
cleanup();
});
describe('ReleaseCandidateBanner', () => {
it('renders RC guidance with release-note and feedback links', () => {
render(() => <ReleaseCandidateBanner version="6.0.0-rc.2" />);
expect(
screen.getByText('Pulse 6.0.0-rc.2 is a public v6 release candidate.'),
).toBeInTheDocument();
expect(
screen.getByText(
/Start in a staging or non-critical environment first, then send feedback/i,
),
).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View release notes' })).toHaveAttribute(
'href',
'https://github.com/rcourtman/Pulse/releases/tag/v6.0.0-rc.2',
);
expect(screen.getByRole('link', { name: 'Send feedback' })).toHaveAttribute(
'href',
'https://github.com/rcourtman/Pulse/issues/new?template=v6_rc_feedback.yml',
);
});
});

View file

@ -1,5 +1,4 @@
const GITHUB_RELEASES_BASE_URL = 'https://github.com/rcourtman/Pulse/releases';
const GITHUB_ISSUES_BASE_URL = 'https://github.com/rcourtman/Pulse/issues/new';
export const normalizeReleaseVersion = (version?: string | null): string => {
const trimmed = (version ?? '').trim();
@ -19,16 +18,6 @@ export const buildReleaseNotesUrl = (version?: string | null): string => {
return tag ? `${GITHUB_RELEASES_BASE_URL}/tag/${tag}` : GITHUB_RELEASES_BASE_URL;
};
export const buildIssueTemplateUrl = (template?: string | null): string => {
const trimmed = (template ?? '').trim();
if (!trimmed) {
return GITHUB_ISSUES_BASE_URL;
}
return `${GITHUB_ISSUES_BASE_URL}?template=${encodeURIComponent(trimmed)}`;
};
export const buildV6RcFeedbackUrl = (): string => buildIssueTemplateUrl('v6_rc_feedback.yml');
export const buildDockerImageTag = (version?: string | null): string => {
const normalized = normalizeReleaseVersion(version);
return normalized || 'latest';

View file

@ -1,43 +0,0 @@
import { expect, test } from '@playwright/test';
import { ensureAuthenticated } from './helpers';
const RC_VERSION_INFO = {
version: '6.0.0-rc.2',
build: '',
runtime: 'unknown',
channel: 'rc',
isDocker: false,
isSourceBuild: true,
isDevelopment: false,
deploymentType: 'source',
};
test.describe('Release candidate banner', () => {
test('renders generic RC copy with version-specific release links', async ({ page }, testInfo) => {
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop runtime proof');
await page.route('**/api/version', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(RC_VERSION_INFO),
}),
);
await ensureAuthenticated(page);
await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
const banner = page.getByRole('status').filter({
hasText: 'Pulse 6.0.0-rc.2 is a public v6 release candidate.',
});
await expect(banner).toContainText('Pulse 6.0.0-rc.2 is a public v6 release candidate.');
await expect(page.getByRole('link', { name: 'View release notes' })).toHaveAttribute(
'href',
'https://github.com/rcourtman/Pulse/releases/tag/v6.0.0-rc.2',
);
await expect(page.getByRole('link', { name: 'Send feedback' })).toHaveAttribute(
'href',
'https://github.com/rcourtman/Pulse/issues/new?template=v6_rc_feedback.yml',
);
});
});

View file

@ -0,0 +1,78 @@
import { expect, test } from '@playwright/test';
import { waitForPulseReady } from './helpers';
const RC_VERSION_INFO = {
version: '6.0.0-rc.2',
build: '',
runtime: 'unknown',
channel: 'rc',
isDocker: false,
isSourceBuild: true,
isDevelopment: false,
deploymentType: 'source',
};
test.describe('Release candidate shell', () => {
test('keeps RC builds free of the public release-candidate banner', async ({ page }, testInfo) => {
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop runtime proof');
await waitForPulseReady(page);
await page.addInitScript(() => {
localStorage.setItem('pulse_whats_new_v2_shown', 'true');
});
await page.route('**/api/security/status', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
hasAuthentication: true,
hideLocalLogin: false,
hasProxyAuth: false,
proxyAuthUsername: '',
proxyAuthLogoutURL: '',
publicAccess: false,
requiresAuth: true,
ssoEnabled: false,
ssoProviders: [],
ssoSessionUsername: '',
sessionCapabilities: {
assistantEnabled: true,
demoMode: false,
},
presentationPolicy: {
demoMode: false,
readOnly: false,
hideCommercial: false,
hideUpgrade: false,
},
}),
}),
);
await page.route('**/api/state', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({}),
}),
);
await page.route('**/api/version', (route) =>
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(RC_VERSION_INFO),
}),
);
await page.goto('/dashboard', { waitUntil: 'domcontentloaded' });
await page.waitForURL(/\/dashboard/, { timeout: 15_000 });
await expect(
page.getByRole('status').filter({ hasText: 'public v6 release candidate' }),
).toHaveCount(0);
await expect(page.getByText('Preview', { exact: true })).toBeVisible();
});
});