FN-7852: add one-time SQLite→embedded Postgres storage notice banner

Adds a dismissible, one-time dashboard banner announcing the upcoming SQLite→embedded-Postgres storage backend change.

- New self-contained StorageMigrationNoticeBanner component with title/body copy and a dismiss control that persists via localStorage key fusion:storage-migration-notice-dismissed
- Wire the banner into DashboardBanners alongside the CLI binary install banner for project-scoped views
- Add en locale strings (storageMigrationNotice.title/body/dismissLabel) in app.json
- Add component test coverage for render/dismiss/persistence behavior
- Document the notice in docs/dashboard-guide.md
- Add a minor changeset for @runfusion/fusion

Files changed:
 .changeset/fn-7852-storage-migration-notice.md     |  7 ++
 docs/dashboard-guide.md                            |  2 +
 .../components/StorageMigrationNoticeBanner.css    | 73 +++++++++++++++++++
 .../components/StorageMigrationNoticeBanner.tsx    | 65 +++++++++++++++++
 .../StorageMigrationNoticeBanner.test.tsx          | 82 ++++++++++++++++++++++
 .../app/components/dashboard/DashboardBanners.tsx  | 11 ++-
 packages/i18n/locales/en/app.json                  |  5 ++
 7 files changed, 242 insertions(+), 3 deletions(-)

Fusion-Task-Id: FN-7852

Fusion-Task-Lineage: e3235cce-9ca3-4830-8733-a2ec46c53246

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-12 11:19:52 -07:00
parent c13d2ee9c2
commit 9bb74595c2
7 changed files with 242 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": minor
---
summary: Add a one-time banner announcing the upcoming SQLite→embedded-Postgres storage change.
category: feature
dev: New self-contained `StorageMigrationNoticeBanner` in the dashboard banner cluster; permanent dismissal via `localStorage` key `fusion:storage-migration-notice-dismissed`.

View File

@@ -789,6 +789,8 @@ Anthropic also supports a raw `ANTHROPIC_API_KEY` from a separate **Anthropic AP
## Setup Warning Banner ## Setup Warning Banner
The dashboard banner cluster can also show a one-time storage notice announcing that the next Fusion version replaces the current SQLite data store with an embedded Postgres backend; dismissing it stores a browser-local acknowledgement so it does not reappear.
The dashboard and New Task modal show setup warnings only after readiness checks finish. AI-provider warnings appear immediately because agents cannot work without a provider. GitHub warnings are delayed per project: Fusion records the first time GitHub OAuth and authenticated `gh` CLI are both missing, waits one day, and then shows **GitHub not connected** if GitHub is still unavailable. Reconnecting GitHub clears the timer so a later disconnect starts a fresh one-day grace period. The dashboard and New Task modal show setup warnings only after readiness checks finish. AI-provider warnings appear immediately because agents cannot work without a provider. GitHub warnings are delayed per project: Fusion records the first time GitHub OAuth and authenticated `gh` CLI are both missing, waits one day, and then shows **GitHub not connected** if GitHub is still unavailable. Reconnecting GitHub clears the timer so a later disconnect starts a fresh one-day grace period.
When the dashboard GitHub warning is visible, its **Connect GitHub** action opens **Settings → Authentication**. The New Task modal keeps immediate AI-provider warnings but suppresses the GitHub warning because that modal does not own the Settings navigation callback required for an actionable GitHub setup control. When the dashboard GitHub warning is visible, its **Connect GitHub** action opens **Settings → Authentication**. The New Task modal keeps immediate AI-provider warnings but suppresses the GitHub warning because that modal does not own the Settings navigation callback required for an actionable GitHub setup control.

View File

@@ -0,0 +1,73 @@
/* === StorageMigrationNoticeBanner === */
.storage-migration-notice-banner {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--space-sm);
margin: var(--space-sm) var(--space-md) 0;
padding: var(--space-sm) var(--space-md);
background: linear-gradient(
90deg,
color-mix(in srgb, var(--todo) 12%, transparent),
color-mix(in srgb, var(--todo) 4%, transparent)
);
border: var(--btn-border-width) solid color-mix(in srgb, var(--todo) 35%, transparent);
border-radius: var(--radius-md);
color: var(--text);
}
.storage-migration-notice-banner__body {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--space-xs);
min-width: 0;
}
.storage-migration-notice-banner__title {
font-weight: 600;
font-size: var(--font-size-base);
color: var(--text);
}
.storage-migration-notice-banner__text {
margin: 0;
font-size: var(--font-size-base);
line-height: 1.5;
color: var(--text-muted);
}
.storage-migration-notice-banner__dismiss {
display: inline-flex;
align-items: center;
justify-content: center;
padding: var(--space-xs);
background: transparent;
color: var(--text-muted);
border: none;
border-radius: var(--radius-sm);
cursor: pointer;
flex-shrink: 0;
line-height: 0;
transition: color var(--transition-fast), background var(--transition-fast);
}
.storage-migration-notice-banner__dismiss:hover {
background: color-mix(in srgb, var(--text) 6%, transparent);
color: var(--text);
}
.storage-migration-notice-banner__dismiss:focus-visible {
outline: none;
box-shadow: var(--focus-ring-strong);
}
@media (max-width: 768px) {
.storage-migration-notice-banner {
align-items: stretch;
}
.storage-migration-notice-banner__dismiss {
align-self: flex-start;
}
}

View File

@@ -0,0 +1,65 @@
import { useCallback, useState } from "react";
import { useTranslation } from "react-i18next";
import { X } from "lucide-react";
import "./StorageMigrationNoticeBanner.css";
export const STORAGE_MIGRATION_NOTICE_DISMISS_KEY = "fusion:storage-migration-notice-dismissed";
/*
FNXC:StorageMigrationNotice 2026-07-12-00:00:
The dashboard must announce the next-version SQLite → embedded Postgres storage change before operators upgrade. Dismissal is global rather than project-scoped because the notice describes an app-wide on-disk storage engine change, not a per-project condition; localStorage persistence makes the one-time acknowledgement permanent on this browser profile.
*/
function isDismissed(): boolean {
if (typeof window === "undefined") return false;
try {
return window.localStorage.getItem(STORAGE_MIGRATION_NOTICE_DISMISS_KEY) === "1";
} catch {
return false;
}
}
function persistDismissal(): void {
if (typeof window === "undefined") return;
try {
window.localStorage.setItem(STORAGE_MIGRATION_NOTICE_DISMISS_KEY, "1");
} catch {
// Ignore quota / private-mode errors — the click still hides this in-memory render.
}
}
/** One-time informational banner for the upcoming dashboard storage backend change. */
export function StorageMigrationNoticeBanner() {
const { t } = useTranslation("app");
const [dismissed, setDismissed] = useState<boolean>(() => isDismissed());
const handleDismiss = useCallback(() => {
persistDismissal();
setDismissed(true);
}, []);
if (dismissed) return null;
return (
<div className="storage-migration-notice-banner" role="status" aria-live="polite">
<div className="storage-migration-notice-banner__body">
<div className="storage-migration-notice-banner__title">
{t("storageMigrationNotice.title", "Storage update coming in the next Fusion version")}
</div>
<p className="storage-migration-notice-banner__text">
{t(
"storageMigrationNotice.body",
"The next Fusion version will replace the current SQLite data store with an embedded Postgres backend for data storage. No migration runs from this notice; it is an advance heads-up for operators who rely on the current .fusion/fusion.db SQLite file.",
)}
</p>
</div>
<button
type="button"
className="storage-migration-notice-banner__dismiss touch-target"
aria-label={t("storageMigrationNotice.dismissLabel", "Dismiss storage update notice")}
onClick={handleDismiss}
>
<X size={16} aria-hidden="true" />
</button>
</div>
);
}

View File

@@ -0,0 +1,82 @@
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
STORAGE_MIGRATION_NOTICE_DISMISS_KEY,
StorageMigrationNoticeBanner,
} from "../StorageMigrationNoticeBanner";
const title = "Storage update coming in the next Fusion version";
const body = /next Fusion version will replace the current SQLite data store with an embedded Postgres backend/i;
const dismissLabel = "Dismiss storage update notice";
const originalLocalStorage = window.localStorage;
describe("StorageMigrationNoticeBanner", () => {
beforeEach(() => {
Object.defineProperty(window, "localStorage", {
configurable: true,
value: originalLocalStorage,
});
window.localStorage.clear();
});
afterEach(() => {
cleanup();
vi.restoreAllMocks();
Object.defineProperty(window, "localStorage", {
configurable: true,
value: originalLocalStorage,
});
window.localStorage.clear();
});
it("renders the storage notice when the dismissal key is absent", () => {
render(<StorageMigrationNoticeBanner />);
expect(screen.getByRole("status")).toBeInTheDocument();
expect(screen.getByText(title)).toBeInTheDocument();
expect(screen.getByText(body)).toBeInTheDocument();
expect(screen.getByRole("button", { name: dismissLabel })).toBeInTheDocument();
});
it("hides immediately and persists the dismissal key when dismissed", () => {
render(<StorageMigrationNoticeBanner />);
fireEvent.click(screen.getByRole("button", { name: dismissLabel }));
expect(window.localStorage.getItem(STORAGE_MIGRATION_NOTICE_DISMISS_KEY)).toBe("1");
expect(screen.queryByRole("status")).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: dismissLabel })).not.toBeInTheDocument();
});
it("returns null on a fresh mount when the dismissal key is already persisted", () => {
window.localStorage.setItem(STORAGE_MIGRATION_NOTICE_DISMISS_KEY, "1");
render(<StorageMigrationNoticeBanner />);
expect(screen.queryByRole("status")).not.toBeInTheDocument();
expect(screen.queryByText(title)).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: dismissLabel })).not.toBeInTheDocument();
});
it("does not crash when localStorage getItem or setItem throws", () => {
Object.defineProperty(window, "localStorage", {
configurable: true,
value: {
getItem: () => {
throw new Error("private mode getItem denied");
},
setItem: () => {
throw new Error("private mode setItem denied");
},
},
});
expect(() => render(<StorageMigrationNoticeBanner />)).not.toThrow();
expect(screen.getByRole("status")).toBeInTheDocument();
expect(() => {
fireEvent.click(screen.getByRole("button", { name: dismissLabel }));
}).not.toThrow();
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
});

View File

@@ -10,6 +10,7 @@ import { EngineStatusBanner } from "../EngineStatusBanner";
import { OAuthReloginBanner } from "../OAuthReloginBanner"; import { OAuthReloginBanner } from "../OAuthReloginBanner";
import { SessionNotificationBanner } from "../SessionNotificationBanner"; import { SessionNotificationBanner } from "../SessionNotificationBanner";
import { CliBinaryInstallBanner } from "../CliBinaryInstallBanner"; import { CliBinaryInstallBanner } from "../CliBinaryInstallBanner";
import { StorageMigrationNoticeBanner } from "../StorageMigrationNoticeBanner";
import { OnboardingResumeCard } from "../OnboardingResumeCard"; import { OnboardingResumeCard } from "../OnboardingResumeCard";
import { PostOnboardingRecommendations } from "../PostOnboardingRecommendations"; import { PostOnboardingRecommendations } from "../PostOnboardingRecommendations";
import { UpdateAvailableBanner } from "../UpdateAvailableBanner"; import { UpdateAvailableBanner } from "../UpdateAvailableBanner";
@@ -101,9 +102,13 @@ export function DashboardBanners({
/> />
)} )}
{viewMode === "project" && currentProject && ( {viewMode === "project" && currentProject && (
<CliBinaryInstallBanner <>
onOpenSettings={() => openSettingsWithNav("general" as SectionId)} {/* FNXC:StorageMigrationNotice 2026-07-12-00:00: Keep the one-time storage-backend announcement beside other project-scoped passive notices while its localStorage dismissal remains app-wide. */}
/> <StorageMigrationNoticeBanner />
<CliBinaryInstallBanner
onOpenSettings={() => openSettingsWithNav("general" as SectionId)}
/>
</>
)} )}
{viewMode === "project" && currentProject && showOnboardingResumeCard && ( {viewMode === "project" && currentProject && showOnboardingResumeCard && (
<OnboardingResumeCard onResume={modalManager.openModelOnboarding} /> <OnboardingResumeCard onResume={modalManager.openModelOnboarding} />

View File

@@ -1420,6 +1420,11 @@
"versionMismatchPrefix": "Your installed", "versionMismatchPrefix": "Your installed",
"versionMismatchSuffix": "Update to stay in sync." "versionMismatchSuffix": "Update to stay in sync."
}, },
"storageMigrationNotice": {
"body": "The next Fusion version will replace the current SQLite data store with an embedded Postgres backend for data storage. No migration runs from this notice; it is an advance heads-up for operators who rely on the current .fusion/fusion.db SQLite file.",
"dismissLabel": "Dismiss storage update notice",
"title": "Storage update coming in the next Fusion version"
},
"cliBinary": { "cliBinary": {
"binaryLabel": "Binary:", "binaryLabel": "Binary:",
"checking": "Checking…", "checking": "Checking…",