fix: prevent false PostgreSQL corruption alerts (#2352)

## Summary

PostgreSQL runtime roles without `CREATE` permission on `public` no
longer trigger schema writes during migration-marker health reads, so
`permission denied for schema public` is not mislabeled as database
corruption. Once connectivity and task-ID integrity pass, an unavailable
migration marker is treated as advisory instead of making the whole
database unhealthy. Dashboard and notification guidance now describes a
PostgreSQL health failure accurately and renders actionable log and
recovery links in every supported locale.

## Validation

- 54 targeted tests passed across core, dashboard, engine, and i18n.
- Typechecks passed for all four affected packages.
- Scoped ESLint, strict changeset validation, and diff checks passed.

---

[![Compound
Engineering](https://img.shields.io/badge/Built_with-Compound_Engineering-6366f1)](https://github.com/EveryInc/compound-engineering-plugin)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* PostgreSQL health failures are now reported as degraded health checks
rather than database corruption.
* Migration-status lookup failures no longer incorrectly mark an
otherwise healthy database as unhealthy.
* Migration-state checks are now read-only and avoid creating or
modifying database structures.

* **UI & Localization**
* Updated database health banner messaging and recovery guidance across
supported languages.
* The banner now appears for broader PostgreSQL health failures and
links to storage documentation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
gsxdsm
2026-07-19 18:59:28 -07:00
committed by GitHub
parent 4ad7c241a6
commit 90b9feb7ae
18 changed files with 243 additions and 45 deletions

View File

@@ -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.

View File

@@ -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);
});
});

View File

@@ -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<Record<string, never>>,
migrationKey: string,
): Promise<SqliteMigrationState | null> {
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<Record<string, never>>,
migrationKey: string,
): Promise<boolean> {
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. */

View File

@@ -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({
<span className="status-dot status-dot--error" aria-hidden="true" />
<AlertTriangle aria-hidden="true" />
<div className="db-corruption-banner__headline-copy">
<h2 className="db-corruption-banner__headline">{t("dbBanner.title", "Database corruption detected")}</h2>
<h2 className="db-corruption-banner__headline">{t("dbBanner.title", "Database health check failed")}</h2>
{checkedAtLabel ? (
<p className="db-corruption-banner__meta">{t("dbBanner.lastChecked", "Last checked: {{checkedAtLabel}}", { checkedAtLabel })}</p>
) : null}
@@ -53,7 +59,7 @@ export function DbCorruptionBanner({
</div>
<p className="db-corruption-banner__body">
{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.")}
</p>
<ul className="db-corruption-banner__list">
@@ -68,10 +74,10 @@ export function DbCorruptionBanner({
<strong className="db-corruption-banner__footer-label">{t("dbBanner.whatToDo", "What to do:")}</strong>{" "}
<Trans
i18nKey="app:dbBanner.instructions"
defaults="Back up the project, run <cmd>fn db vacuum</cmd> to compact and verify the database, and restore from a known-good backup if corruption persists. See <docsLink>docs/storage.md</docsLink> for the storage layout and recovery guidance."
defaults="Back up the project, check <cmd>database logs</cmd> and PostgreSQL connectivity and permissions, then refresh health. See <docsLink>docs/storage.md</docsLink> for storage and recovery guidance."
components={{
cmd: <code />,
docsLink: <a href="docs/storage.md" />,
docsLink: <a href="/docs/storage.md" />,
}}
/>
</p>

View File

@@ -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", () => {

View File

@@ -188,7 +188,7 @@ export function DashboardBanners({
}}
/>
)}
{viewMode === "project" && currentProject && dashboardHealth?.database?.corruptionDetected === true && (
{viewMode === "project" && currentProject && dashboardHealth?.database?.healthy === false && (
<DbCorruptionBanner
errors={dashboardHealth.database.corruptionErrors}
lastCheckedAt={dashboardHealth.database.lastCheckedAt}

View File

@@ -30,7 +30,9 @@ vi.mock("../../PostOnboardingRecommendations", () => ({ 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: () => <section data-testid="database-health-banner" />,
}));
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(
<DashboardBanners
{...buildProps({
sessionsNeedingInput: [],
dashboardHealth: {
status: "degraded",
engine: { available: true },
database: {
healthy: false,
corruptionDetected: false,
corruptionErrors: ["PostgreSQL health layer unavailable"],
lastCheckedAt: null,
},
taskIdIntegrity: { status: "error", anomalies: [] },
} as DashboardBannersProps["dashboardHealth"],
})}
/>,
);
expect(screen.getByTestId("database-health-banner")).toBeInTheDocument();
});
});
function buildSession(overrides: Partial<AiSessionSummary> = {}): AiSessionSummary {
return {
id: overrides.id ?? "session-1",

View File

@@ -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();
});
});

View File

@@ -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,

View File

@@ -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);

View File

@@ -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 <cmd>database logs</cmd> and PostgreSQL connectivity and permissions, then refresh health. See <docsLink>docs/storage.md</docsLink> 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": {

View File

@@ -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 <cmd>registros de la base de datos</cmd> y la conectividad y los permisos de PostgreSQL, y después actualice el estado. Consulte <docsLink>docs/storage.md</docsLink> 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": {

View File

@@ -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 <cmd>journaux de la base de données</cmd> ainsi que la connectivité et les autorisations PostgreSQL, puis actualisez l'état. Consultez <docsLink>docs/storage.md</docsLink> 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": {

View File

@@ -2009,12 +2009,12 @@
"title": "프로젝트 대시보드"
},
"dbBanner": {
"body": "Fusion의 백그라운드 SQLite 무결성 검사에서 손상이 발견되었습니다. 중요한 작업을 계속하기 전에 아래의 실패한 개체를 검토하세요.",
"instructions": "프로젝트를 백업하고, 데이터베이스가 정상적으로 열리면 {{cmd}}를 시도해 보세요. 손상이 지속될 경우 알려진 정상 백업에서 복원하세요. 저장소 레이아웃 및 복구 안내는 {{link}}를 참고하세요.",
"body": "Fusion의 PostgreSQL 상태 검사에서 오류가 보고되었습니다. 중요한 작업을 계속하기 전에 아래 세부 정보를 검토하세요.",
"instructions": "프로젝트를 백업하고 <cmd>데이터베이스 로그</cmd>와 PostgreSQL 연결 및 권한을 확인한 다음 상태를 새로 고치세요. 저장소 및 복구 지침은 <docsLink>docs/storage.md</docsLink>를 참조하세요.",
"lastChecked": "마지막 확인: {{checkedAtLabel}}",
"refreshHealth": "상태 새로 고침",
"refreshing": "새로 고치는 중…",
"title": "데이터베이스 손상 감지됨",
"title": "데이터베이스 상태 검사 실패",
"whatToDo": "조치 방법:"
},
"deepLink": {

View File

@@ -2009,12 +2009,12 @@
"title": "项目仪表板"
},
"dbBanner": {
"body": "Fusion 的后台 SQLite 完整性检查报告有损坏。继续关键操作之前,请查看下面的失败对象。",
"instructions": "备份项目,如果数据库仍然可以干净地打开,请尝试 {{cmd}},如果损坏仍然存在,请从已知的良好备份中恢复。有关存储布局和恢复指导,请参阅 {{link}}。",
"body": "Fusion 的 PostgreSQL 健康检查报告失败。在继续关键操作之前,请检查下面的详细信息。",
"instructions": "请备份项目,检查<cmd>数据库日志</cmd>以及 PostgreSQL 连接和权限,然后刷新健康状态。有关存储和恢复指南,请参阅 <docsLink>docs/storage.md</docsLink>。",
"lastChecked": "上次检查:{{checkedAtLabel}}",
"refreshHealth": "刷新健康状态",
"refreshing": "正在刷新…",
"title": "检测到数据库损坏",
"title": "数据库健康检查失败",
"whatToDo": "该怎么做:"
},
"deepLink": {

View File

@@ -2009,12 +2009,12 @@
"title": "專案儀表板"
},
"dbBanner": {
"body": "Fusion 的背景 SQLite 完整性檢查報告有損毀。繼續重要操作之前,請查看下面的失敗物件。",
"instructions": "備份專案,如果資料庫仍然可以乾淨地開啟,請嘗試 {{cmd}},如果損毀仍然存在,請從已知的良好備份中恢復。有關儲存配置和恢復指導,請參閱 {{link}}。",
"body": "Fusion 的 PostgreSQL 健康檢查回報失敗。在繼續關鍵操作前,請檢查下方詳細資訊。",
"instructions": "請備份專案,檢查<cmd>資料庫記錄</cmd>以及 PostgreSQL 連線與權限,然後重新整理健康狀態。關於儲存與復原指南,請參閱 <docsLink>docs/storage.md</docsLink>。",
"lastChecked": "上次檢查:{{checkedAtLabel}}",
"refreshHealth": "重新整理健康狀態",
"refreshing": "正在重新整理…",
"title": "偵測到資料庫損毀",
"title": "資料庫健康檢查失敗",
"whatToDo": "該怎麼做:"
},
"deepLink": {

View File

@@ -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("<cmd>");
expect(catalog.dbBanner.instructions).toContain("</cmd>");
expect(catalog.dbBanner.instructions).toContain("<docsLink>docs/storage.md</docsLink>");
expect(catalog.dbBanner.instructions).not.toMatch(/\{\{(?:cmd|link)\}\}/);
});
});

View File

@@ -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 <cmd>database logs</cmd> and PostgreSQL connectivity and permissions, then refresh health. See <docsLink>docs/storage.md</docsLink> 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": {