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:
7
.changeset/fn-7852-storage-migration-notice.md
Normal file
7
.changeset/fn-7852-storage-migration-notice.md
Normal 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`.
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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} />
|
||||
|
||||
@@ -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…",
|
||||
|
||||
Reference in New Issue
Block a user