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
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.
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 { SessionNotificationBanner } from "../SessionNotificationBanner";
import { CliBinaryInstallBanner } from "../CliBinaryInstallBanner";
import { StorageMigrationNoticeBanner } from "../StorageMigrationNoticeBanner";
import { OnboardingResumeCard } from "../OnboardingResumeCard";
import { PostOnboardingRecommendations } from "../PostOnboardingRecommendations";
import { UpdateAvailableBanner } from "../UpdateAvailableBanner";
@@ -101,9 +102,13 @@ export function DashboardBanners({
/>
)}
{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 && (
<OnboardingResumeCard onResume={modalManager.openModelOnboarding} />

View File

@@ -1420,6 +1420,11 @@
"versionMismatchPrefix": "Your installed",
"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": {
"binaryLabel": "Binary:",
"checking": "Checking…",