diff --git a/.changeset/postgres-health-false-corruption.md b/.changeset/postgres-health-false-corruption.md
new file mode 100644
index 0000000000..fd586520bc
--- /dev/null
+++ b/.changeset/postgres-health-false-corruption.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Report PostgreSQL health failures accurately without false database-corruption guidance.
+category: fix
+dev: Makes migration-marker reads non-mutating and treats marker permission failures as advisory.
diff --git a/packages/core/src/__tests__/postgres/sqlite-migration-state.test.ts b/packages/core/src/__tests__/postgres/sqlite-migration-state.test.ts
new file mode 100644
index 0000000000..4747668369
--- /dev/null
+++ b/packages/core/src/__tests__/postgres/sqlite-migration-state.test.ts
@@ -0,0 +1,87 @@
+import { describe, expect, it, vi } from "vitest";
+
+import {
+ getSqliteMigrationState,
+ isSqliteMigrationComplete,
+} from "../../postgres/sqlite-migrator.js";
+
+function queryText(query: unknown): string {
+ return (query as { queryChunks?: Array<{ value: string[] }> }).queryChunks
+ ?.flatMap((chunk) => chunk.value).join("") ?? "";
+}
+
+describe("SQLite migration state reads", () => {
+ function recordingDb(results: unknown[][]) {
+ const statements: string[] = [];
+ const execute = vi.fn(async (query: unknown) => {
+ statements.push(queryText(query));
+ return results.shift() ?? [];
+ });
+ return { db: { execute }, statements };
+ }
+
+ function expectReadOnly(statements: string[]) {
+ expect(statements).not.toEqual([]);
+ for (const statement of statements) {
+ expect(statement).not.toMatch(/\b(?:CREATE|INSERT|UPDATE|DELETE|ALTER|DROP)\b/i);
+ }
+ }
+
+ it("treats a missing marker table as no state without issuing DDL", async () => {
+ const { db, statements } = recordingDb([[{ exists: false }]]);
+
+ await expect(getSqliteMigrationState(db as never, "project:demo")).resolves.toBeNull();
+ expect(statements).toHaveLength(1);
+ expect(statements[0]).toContain("to_regclass");
+ expectReadOnly(statements);
+ });
+
+ it("returns no state when the marker table exists without the requested key", async () => {
+ const { db, statements } = recordingDb([[{ exists: true }], []]);
+
+ await expect(getSqliteMigrationState(db as never, "project:demo")).resolves.toBeNull();
+ expect(statements).toHaveLength(2);
+ expectReadOnly(statements);
+ });
+
+ it("maps an existing marker without issuing writes", async () => {
+ const marker = {
+ migration_key: "project:demo",
+ project_id: "demo",
+ status: "failed" as const,
+ last_error: "copy failed",
+ updated_at: "2026-07-19T00:00:00.000Z",
+ };
+ const { db, statements } = recordingDb([[{ exists: true }], [marker]]);
+
+ await expect(getSqliteMigrationState(db as never, "project:demo")).resolves.toEqual({
+ migrationKey: marker.migration_key,
+ projectId: marker.project_id,
+ status: marker.status,
+ lastError: marker.last_error,
+ updatedAt: marker.updated_at,
+ });
+ expectReadOnly(statements);
+ });
+
+ it.each([
+ [undefined, false],
+ ["running", false],
+ ["failed", false],
+ ["complete", true],
+ ] as const)("reports %s markers as complete=%s without issuing DDL", async (status, expected) => {
+ const results = status === undefined
+ ? [[{ exists: false }]]
+ : [[{ exists: true }], [{
+ migration_key: "project:demo",
+ project_id: "demo",
+ status,
+ last_error: null,
+ updated_at: "2026-07-19T00:00:00.000Z",
+ }]];
+ const { db, statements } = recordingDb(results);
+
+ await expect(isSqliteMigrationComplete(db as never, "project:demo")).resolves.toBe(expected);
+ expectReadOnly(statements);
+ });
+});
diff --git a/packages/core/src/postgres/sqlite-migrator.ts b/packages/core/src/postgres/sqlite-migrator.ts
index 571c94efa5..23b494bba4 100644
--- a/packages/core/src/postgres/sqlite-migrator.ts
+++ b/packages/core/src/postgres/sqlite-migrator.ts
@@ -472,12 +472,18 @@ export interface SqliteMigrationState {
* Dashboard health reads the authoritative per-project cutover marker after
* listen. A running or failed marker is never aged out here: progress ticks do
* not update updated_at, and post-listen running means completion was missed.
+ * Reads must not create the marker table: dashboard and startup connections can
+ * use a runtime role that intentionally lacks CREATE permission in public.
*/
export async function getSqliteMigrationState(
db: PostgresJsDatabase>,
migrationKey: string,
): Promise {
- await ensureMigrationStateTable(db);
+ const tableRows = (await db.execute(sql`
+ SELECT to_regclass('public.${sql.raw(SQLITE_MIGRATION_STATE_TABLE)}') IS NOT NULL AS exists
+ `)) as unknown as Array<{ exists: boolean }>;
+ if (!tableRows[0]?.exists) return null;
+
const rows = (await db.execute(sql`
SELECT migration_key, project_id, status, last_error, updated_at
FROM public.${sql.identifier(SQLITE_MIGRATION_STATE_TABLE)}
@@ -507,12 +513,7 @@ export async function isSqliteMigrationComplete(
db: PostgresJsDatabase>,
migrationKey: string,
): Promise {
- await ensureMigrationStateTable(db);
- const rows = (await db.execute(sql`
- SELECT status FROM public.${sql.identifier(SQLITE_MIGRATION_STATE_TABLE)}
- WHERE migration_key = ${migrationKey}
- `)) as unknown as Array<{ status: string }>;
- return rows[0]?.status === "complete";
+ return (await getSqliteMigrationState(db, migrationKey))?.status === "complete";
}
/** Mark caller-side stamping and verification complete for a durable cutover. */
diff --git a/packages/dashboard/app/components/DbCorruptionBanner.tsx b/packages/dashboard/app/components/DbCorruptionBanner.tsx
index ffd2ac44ca..6ec62d38d1 100644
--- a/packages/dashboard/app/components/DbCorruptionBanner.tsx
+++ b/packages/dashboard/app/components/DbCorruptionBanner.tsx
@@ -11,6 +11,12 @@ interface DbCorruptionBannerProps {
refreshError: string | null;
}
+/*
+FNXC:PostgresHealth 2026-07-19-17:55:
+The shared degraded-health shape includes connectivity, permissions, and query
+failures, so the banner must not claim corruption or prescribe SQLite recovery.
+Keep operator guidance focused on PostgreSQL diagnostics and the reported error.
+*/
export function DbCorruptionBanner({
errors,
lastCheckedAt,
@@ -33,7 +39,7 @@ export function DbCorruptionBanner({
-
{t("dbBanner.title", "Database corruption detected")}
+
{t("dbBanner.title", "Database health check failed")}
{checkedAtLabel ? (
{t("dbBanner.lastChecked", "Last checked: {{checkedAtLabel}}", { checkedAtLabel })}
) : null}
@@ -53,7 +59,7 @@ export function DbCorruptionBanner({
- {t("dbBanner.body", "Fusion's database health check reported the backend is unreachable or corrupt. Review the failing objects below before continuing critical operations.")}
+ {t("dbBanner.body", "Fusion's PostgreSQL health check reported a failure. Review the details below before continuing critical operations.")}
@@ -68,10 +74,10 @@ export function DbCorruptionBanner({
{t("dbBanner.whatToDo", "What to do:")}{" "}
,
- docsLink: ,
+ docsLink: ,
}}
/>
diff --git a/packages/dashboard/app/components/__tests__/DbCorruptionBanner.test.tsx b/packages/dashboard/app/components/__tests__/DbCorruptionBanner.test.tsx
index e36cd64038..e40ff663ca 100644
--- a/packages/dashboard/app/components/__tests__/DbCorruptionBanner.test.tsx
+++ b/packages/dashboard/app/components/__tests__/DbCorruptionBanner.test.tsx
@@ -16,11 +16,14 @@ describe("DbCorruptionBanner", () => {
);
expect(screen.getByRole("alert")).toBeInTheDocument();
- expect(screen.getByText("Database corruption detected")).toBeInTheDocument();
+ expect(screen.getByText("Database health check failed")).toBeInTheDocument();
expect(screen.getByText(/Last checked:/)).toBeInTheDocument();
expect(screen.getByText("bad row")).toBeInTheDocument();
expect(screen.getByText("bad index")).toBeInTheDocument();
- expect(screen.getByRole("link", { name: "docs/storage.md" })).toHaveAttribute("href", "docs/storage.md");
+ expect(screen.getByText(/PostgreSQL health check reported a failure/)).toBeInTheDocument();
+ expect(screen.getByText("database logs")).toBeInTheDocument();
+ expect(screen.getByRole("link", { name: "docs/storage.md" })).toHaveAttribute("href", "/docs/storage.md");
+ expect(screen.queryByText(/\[object Object\]|\{\{link\}\}/)).not.toBeInTheDocument();
});
it("calls onRefresh when the button is clicked", () => {
diff --git a/packages/dashboard/app/components/dashboard/DashboardBanners.tsx b/packages/dashboard/app/components/dashboard/DashboardBanners.tsx
index c738566af4..84679eaea3 100644
--- a/packages/dashboard/app/components/dashboard/DashboardBanners.tsx
+++ b/packages/dashboard/app/components/dashboard/DashboardBanners.tsx
@@ -188,7 +188,7 @@ export function DashboardBanners({
}}
/>
)}
- {viewMode === "project" && currentProject && dashboardHealth?.database?.corruptionDetected === true && (
+ {viewMode === "project" && currentProject && dashboardHealth?.database?.healthy === false && (
({ PostOnboardingRecommenda
vi.mock("../../UpdateAvailableBanner", () => ({ UpdateAvailableBanner: () => null }));
vi.mock("../../MergeAdvanceNotice", () => ({ default: () => null }));
vi.mock("../../TaskIdIntegrityBanner", () => ({ TaskIdIntegrityBanner: () => null }));
-vi.mock("../../DbCorruptionBanner", () => ({ DbCorruptionBanner: () => null }));
+vi.mock("../../DbCorruptionBanner", () => ({
+ DbCorruptionBanner: () => ,
+}));
vi.mock("../../SetupWarningBanner", () => ({
SetupWarningBanner: ({
hasAiProvider,
@@ -76,6 +78,31 @@ describe("isMigrationStatusBannerActive", () => {
});
});
+describe("DashboardBanners database health visibility", () => {
+ it("renders degraded PostgreSQL health without claiming corruption", () => {
+ render(
+ ,
+ );
+
+ expect(screen.getByTestId("database-health-banner")).toBeInTheDocument();
+ });
+});
+
function buildSession(overrides: Partial = {}): AiSessionSummary {
return {
id: overrides.id ?? "session-1",
diff --git a/packages/dashboard/src/__tests__/dashboard-postgres-health.test.ts b/packages/dashboard/src/__tests__/dashboard-postgres-health.test.ts
index 984265a9a5..d0c7817ede 100644
--- a/packages/dashboard/src/__tests__/dashboard-postgres-health.test.ts
+++ b/packages/dashboard/src/__tests__/dashboard-postgres-health.test.ts
@@ -106,7 +106,7 @@ describe("evaluateDashboardPostgresHealth", () => {
expect(healthMocks.checkPostgresHealth).not.toHaveBeenCalled();
expect(result.database).toMatchObject({
healthy: false,
- corruptionDetected: true,
+ corruptionDetected: false,
corruptionErrors: ["PostgreSQL health layer unavailable"],
});
expect(result.taskIdIntegrity).toMatchObject({
@@ -123,7 +123,7 @@ describe("evaluateDashboardPostgresHealth", () => {
expect(result.database).toMatchObject({
healthy: false,
- corruptionDetected: true,
+ corruptionDetected: false,
corruptionErrors: ["PostgreSQL task-ID integrity check failed: integrity query timed out"],
});
expect(result.taskIdIntegrity).toMatchObject({
@@ -131,4 +131,21 @@ describe("evaluateDashboardPostgresHealth", () => {
error: "PostgreSQL task-ID integrity check failed: integrity query timed out",
});
});
+
+ it("keeps advisory migration-status failures from degrading proven database health", async () => {
+ const store = { getAsyncLayer: () => layer, getRootDir: () => "/repo" } as TaskStore;
+ healthMocks.getSqliteMigrationState.mockRejectedValue(
+ new Error("Failed query: SELECT migration marker", { cause: new Error("permission denied") }),
+ );
+
+ const result = await evaluateDashboardPostgresHealth(store);
+
+ expect(result.database).toMatchObject({
+ healthy: true,
+ corruptionDetected: false,
+ corruptionErrors: [],
+ });
+ expect(result.taskIdIntegrity).toMatchObject({ status: "ok", anomalies: [] });
+ expect(result.migration).toBeUndefined();
+ });
});
diff --git a/packages/dashboard/src/dashboard-postgres-health.ts b/packages/dashboard/src/dashboard-postgres-health.ts
index b0f5ba5bcc..8c06fa4727 100644
--- a/packages/dashboard/src/dashboard-postgres-health.ts
+++ b/packages/dashboard/src/dashboard-postgres-health.ts
@@ -75,19 +75,26 @@ export async function evaluateDashboardPostgresHealth(
]);
if (errors.length > 0) return failedHealth(checkedAt, ...errors);
+ let taskIdIntegrity: DashboardTaskIdIntegrityHealth;
+ try {
+ taskIdIntegrity = await detectTaskIdIntegrityAnomaliesAsync(layer.db);
+ } catch (error) {
+ return failedHealth(
+ checkedAt,
+ `PostgreSQL task-ID integrity check failed: ${errorMessage(error)}`,
+ );
+ }
+
try {
- const taskIdIntegrity = await detectTaskIdIntegrityAnomaliesAsync(layer.db);
const migration = await resolveDashboardMigrationHealth(store, layer, context);
return {
database: healthyDatabase(checkedAt),
taskIdIntegrity,
...(migration ? { migration } : {}),
};
- } catch (error) {
- return failedHealth(
- checkedAt,
- `PostgreSQL task-ID integrity check failed: ${errorMessage(error)}`,
- );
+ } catch {
+ // Migration status is advisory after connectivity and task-ID queries pass.
+ return { database: healthyDatabase(checkedAt), taskIdIntegrity };
}
}
@@ -147,7 +154,7 @@ function failedHealth(
return {
database: {
healthy: false,
- corruptionDetected: true,
+ corruptionDetected: false,
corruptionErrors: visibleErrors,
lastCheckedAt: checkedAt,
isRunning: false,
diff --git a/packages/engine/src/__tests__/self-healing-db-corruption.test.ts b/packages/engine/src/__tests__/self-healing-db-corruption.test.ts
index abca83d87f..ab07bc9f4a 100644
--- a/packages/engine/src/__tests__/self-healing-db-corruption.test.ts
+++ b/packages/engine/src/__tests__/self-healing-db-corruption.test.ts
@@ -183,6 +183,29 @@ describe("FN-5284: self-healing DB corruption surfacing", () => {
);
});
+ it("uses SQLite corruption wording for the fallback ntfy notification", async () => {
+ const store = createMockStore({
+ getDatabaseHealth: vi.fn().mockReturnValue({
+ healthy: false,
+ corruptionDetected: true,
+ corruptionErrors: ["connection refused"],
+ lastCheckedAt: new Date("2026-05-20T00:05:00.000Z"),
+ isRunning: false,
+ }),
+ });
+ const manager = new SelfHealingManager(store, { rootDir: "/tmp/test-project" });
+ stubMaintenance(manager);
+
+ await (manager as any).runMaintenance();
+
+ expect(notifierModule.sendNtfyNotification).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: "Database corruption detected",
+ message: "Background SQLite integrity check detected corruption. Errors: connection refused.",
+ }),
+ );
+ });
+
it("respects the cooldown and avoids duplicate dispatches and audits", async () => {
const dispatch = vi.fn().mockResolvedValue(undefined);
vi.spyOn(notifierModule, "getActiveNotificationService").mockReturnValue({ dispatch } as unknown as NotificationService);
diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json
index 7b6dab28f5..b0e8d103a5 100644
--- a/packages/i18n/locales/en/app.json
+++ b/packages/i18n/locales/en/app.json
@@ -2006,12 +2006,12 @@
"updatingVersion": "Updating to a new frontend version..."
},
"dbBanner": {
- "body": "Fusion's background SQLite integrity check reported corruption. Review the failing objects below before continuing critical operations.",
- "instructions": "Back up the project, try {{cmd}} if the database still opens cleanly, and restore from a known-good backup if corruption persists. See {{link}} for the storage layout and recovery guidance.",
+ "body": "Fusion's PostgreSQL health check reported a failure. Review the details below before continuing critical operations.",
+ "instructions": "Back up the project, check database logs and PostgreSQL connectivity and permissions, then refresh health. See docs/storage.md for storage and recovery guidance.",
"lastChecked": "Last checked: {{checkedAtLabel}}",
"refreshHealth": "Refresh health",
"refreshing": "Refreshing…",
- "title": "Database corruption detected",
+ "title": "Database health check failed",
"whatToDo": "What to do:"
},
"deepLink": {
diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json
index c6c7ae5f2d..22e90aefc6 100644
--- a/packages/i18n/locales/es/app.json
+++ b/packages/i18n/locales/es/app.json
@@ -2009,12 +2009,12 @@
"title": "Panel del proyecto"
},
"dbBanner": {
- "body": "La verificación de integridad de SQLite de Fusion en segundo plano informó de corrupción. Revise los objetos con error a continuación antes de continuar con operaciones críticas.",
- "instructions": "Haga copia de seguridad del proyecto, intente {{cmd}} si la base de datos aún se abre correctamente, y restaure desde una copia de seguridad conocida si la corrupción persiste. Consulte {{link}} para conocer la disposición del almacenamiento y la orientación de recuperación.",
+ "body": "La comprobación de estado de PostgreSQL de Fusion informó de un fallo. Revise los detalles antes de continuar con operaciones críticas.",
+ "instructions": "Haga una copia de seguridad del proyecto, revise los registros de la base de datos y la conectividad y los permisos de PostgreSQL, y después actualice el estado. Consulte docs/storage.md para obtener orientación sobre almacenamiento y recuperación.",
"lastChecked": "Última comprobación: {{checkedAtLabel}}",
"refreshHealth": "Actualizar estado",
"refreshing": "Actualizando…",
- "title": "Corrupción de base de datos detectada",
+ "title": "Falló la comprobación de estado de la base de datos",
"whatToDo": "Qué hacer:"
},
"deepLink": {
diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json
index 1266116436..21a5b4b23f 100644
--- a/packages/i18n/locales/fr/app.json
+++ b/packages/i18n/locales/fr/app.json
@@ -2009,12 +2009,12 @@
"title": "Tableau de bord du projet"
},
"dbBanner": {
- "body": "La vérification d'intégrité SQLite en arrière-plan de Fusion a signalé une corruption. Veuillez examiner les objets défaillants ci-dessous avant de continuer les opérations critiques.",
- "instructions": "Sauvegardez le projet, essayez {{cmd}} si la base de données s'ouvre toujours correctement, et restaurez à partir d'une sauvegarde connue bonne si la corruption persiste. Consultez {{link}} pour la disposition du stockage et les conseils de récupération.",
+ "body": "La vérification de l'état de PostgreSQL par Fusion a signalé un échec. Examinez les détails avant de poursuivre les opérations critiques.",
+ "instructions": "Sauvegardez le projet, consultez les journaux de la base de données ainsi que la connectivité et les autorisations PostgreSQL, puis actualisez l'état. Consultez docs/storage.md pour les conseils de stockage et de récupération.",
"lastChecked": "Dernier contrôle : {{checkedAtLabel}}",
"refreshHealth": "Actualiser la santé",
"refreshing": "Actualisation…",
- "title": "Corruption de base de données détectée",
+ "title": "Échec de la vérification de l'état de la base de données",
"whatToDo": "Que faire :"
},
"deepLink": {
diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json
index c0fc7f6551..5b5d9b720b 100644
--- a/packages/i18n/locales/ko/app.json
+++ b/packages/i18n/locales/ko/app.json
@@ -2009,12 +2009,12 @@
"title": "프로젝트 대시보드"
},
"dbBanner": {
- "body": "Fusion의 백그라운드 SQLite 무결성 검사에서 손상이 발견되었습니다. 중요한 작업을 계속하기 전에 아래의 실패한 개체를 검토하세요.",
- "instructions": "프로젝트를 백업하고, 데이터베이스가 정상적으로 열리면 {{cmd}}를 시도해 보세요. 손상이 지속될 경우 알려진 정상 백업에서 복원하세요. 저장소 레이아웃 및 복구 안내는 {{link}}를 참고하세요.",
+ "body": "Fusion의 PostgreSQL 상태 검사에서 오류가 보고되었습니다. 중요한 작업을 계속하기 전에 아래 세부 정보를 검토하세요.",
+ "instructions": "프로젝트를 백업하고 데이터베이스 로그와 PostgreSQL 연결 및 권한을 확인한 다음 상태를 새로 고치세요. 저장소 및 복구 지침은 docs/storage.md를 참조하세요.",
"lastChecked": "마지막 확인: {{checkedAtLabel}}",
"refreshHealth": "상태 새로 고침",
"refreshing": "새로 고치는 중…",
- "title": "데이터베이스 손상 감지됨",
+ "title": "데이터베이스 상태 검사 실패",
"whatToDo": "조치 방법:"
},
"deepLink": {
diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json
index 7351f3cf2e..e480cb5768 100644
--- a/packages/i18n/locales/zh-CN/app.json
+++ b/packages/i18n/locales/zh-CN/app.json
@@ -2009,12 +2009,12 @@
"title": "项目仪表板"
},
"dbBanner": {
- "body": "Fusion 的后台 SQLite 完整性检查报告有损坏。继续关键操作之前,请查看下面的失败对象。",
- "instructions": "备份项目,如果数据库仍然可以干净地打开,请尝试 {{cmd}},如果损坏仍然存在,请从已知的良好备份中恢复。有关存储布局和恢复指导,请参阅 {{link}}。",
+ "body": "Fusion 的 PostgreSQL 健康检查报告失败。在继续关键操作之前,请检查下面的详细信息。",
+ "instructions": "请备份项目,检查数据库日志以及 PostgreSQL 连接和权限,然后刷新健康状态。有关存储和恢复指南,请参阅 docs/storage.md。",
"lastChecked": "上次检查:{{checkedAtLabel}}",
"refreshHealth": "刷新健康状态",
"refreshing": "正在刷新…",
- "title": "检测到数据库损坏",
+ "title": "数据库健康检查失败",
"whatToDo": "该怎么做:"
},
"deepLink": {
diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json
index 468c182f79..9f12396c13 100644
--- a/packages/i18n/locales/zh-TW/app.json
+++ b/packages/i18n/locales/zh-TW/app.json
@@ -2009,12 +2009,12 @@
"title": "專案儀表板"
},
"dbBanner": {
- "body": "Fusion 的背景 SQLite 完整性檢查報告有損毀。繼續重要操作之前,請查看下面的失敗物件。",
- "instructions": "備份專案,如果資料庫仍然可以乾淨地開啟,請嘗試 {{cmd}},如果損毀仍然存在,請從已知的良好備份中恢復。有關儲存配置和恢復指導,請參閱 {{link}}。",
+ "body": "Fusion 的 PostgreSQL 健康檢查回報失敗。在繼續關鍵操作前,請檢查下方詳細資訊。",
+ "instructions": "請備份專案,檢查資料庫記錄以及 PostgreSQL 連線與權限,然後重新整理健康狀態。關於儲存與復原指南,請參閱 docs/storage.md。",
"lastChecked": "上次檢查:{{checkedAtLabel}}",
"refreshHealth": "重新整理健康狀態",
"refreshing": "正在重新整理…",
- "title": "偵測到資料庫損毀",
+ "title": "資料庫健康檢查失敗",
"whatToDo": "該怎麼做:"
},
"deepLink": {
diff --git a/packages/i18n/src/__tests__/db-banner-catalog.test.ts b/packages/i18n/src/__tests__/db-banner-catalog.test.ts
new file mode 100644
index 0000000000..4258f9e27b
--- /dev/null
+++ b/packages/i18n/src/__tests__/db-banner-catalog.test.ts
@@ -0,0 +1,20 @@
+import { readFileSync } from "node:fs";
+import { describe, expect, it } from "vitest";
+
+const locales = ["en", "es", "fr", "ko", "zh-CN", "zh-TW"] as const;
+
+describe("database health banner catalogs", () => {
+ it.each(locales)("uses renderable rich-text guidance in %s", (locale) => {
+ const catalogPath = new URL(`../../locales/${locale}/app.json`, import.meta.url);
+ const catalog = JSON.parse(readFileSync(catalogPath, "utf8")) as {
+ dbBanner: { body: string; instructions: string; title: string };
+ };
+
+ expect(catalog.dbBanner.title).toBeTruthy();
+ expect(catalog.dbBanner.body).toBeTruthy();
+ expect(catalog.dbBanner.instructions).toContain("");
+ expect(catalog.dbBanner.instructions).toContain("");
+ expect(catalog.dbBanner.instructions).toContain("docs/storage.md");
+ expect(catalog.dbBanner.instructions).not.toMatch(/\{\{(?:cmd|link)\}\}/);
+ });
+});
diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts
index 16ac4e3954..bb2e95aa15 100644
--- a/packages/i18n/src/resources.d.ts
+++ b/packages/i18n/src/resources.d.ts
@@ -1973,12 +1973,12 @@ export default interface Resources {
"updatingVersion": "Updating to a new frontend version..."
},
"dbBanner": {
- "body": "Fusion's background SQLite integrity check reported corruption. Review the failing objects below before continuing critical operations.",
- "instructions": "Back up the project, try {{cmd}} if the database still opens cleanly, and restore from a known-good backup if corruption persists. See {{link}} for the storage layout and recovery guidance.",
+ "body": "Fusion's PostgreSQL health check reported a failure. Review the details below before continuing critical operations.",
+ "instructions": "Back up the project, check database logs and PostgreSQL connectivity and permissions, then refresh health. See docs/storage.md for storage and recovery guidance.",
"lastChecked": "Last checked: {{checkedAtLabel}}",
"refreshHealth": "Refresh health",
"refreshing": "Refreshing…",
- "title": "Database corruption detected",
+ "title": "Database health check failed",
"whatToDo": "What to do:"
},
"deepLink": {