Files
fusion/packages/dashboard/app/components/DbCorruptionBanner.tsx
gsxdsm 90b9feb7ae 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 -->
2026-07-19 18:59:28 -07:00

88 lines
3.4 KiB
TypeScript

import { AlertTriangle, RefreshCw } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";
import "./DbCorruptionBanner.css";
interface DbCorruptionBannerProps {
errors: string[];
lastCheckedAt: string | null;
onRefresh: () => void | Promise<void>;
refreshing: boolean;
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,
onRefresh,
refreshing,
refreshError,
}: DbCorruptionBannerProps) {
const { t } = useTranslation("app");
if (errors.length === 0) {
return null;
}
const visibleErrors = errors.slice(0, 5);
const checkedAtLabel = lastCheckedAt ? new Date(lastCheckedAt).toLocaleString() : null;
return (
<section className="db-corruption-banner" role="alert" aria-live="assertive">
<div className="db-corruption-banner__header">
<div className="db-corruption-banner__headline-wrap">
<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 health check failed")}</h2>
{checkedAtLabel ? (
<p className="db-corruption-banner__meta">{t("dbBanner.lastChecked", "Last checked: {{checkedAtLabel}}", { checkedAtLabel })}</p>
) : null}
</div>
</div>
<button
type="button"
className="btn btn-sm db-corruption-banner__refresh"
onClick={() => {
void onRefresh();
}}
disabled={refreshing}
>
<RefreshCw className={refreshing ? "db-corruption-banner__refresh-icon db-corruption-banner__refresh-icon--spinning" : "db-corruption-banner__refresh-icon"} aria-hidden="true" />
{refreshing ? t("dbBanner.refreshing", "Refreshing…") : t("dbBanner.refreshHealth", "Refresh health")}
</button>
</div>
<p className="db-corruption-banner__body">
{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">
{visibleErrors.map((error, index) => (
<li key={`${index}:${error}`} className="db-corruption-banner__item">
<code className="db-corruption-banner__error-code">{error}</code>
</li>
))}
</ul>
<p className="db-corruption-banner__footer">
<strong className="db-corruption-banner__footer-label">{t("dbBanner.whatToDo", "What to do:")}</strong>{" "}
<Trans
i18nKey="app:dbBanner.instructions"
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" />,
}}
/>
</p>
{refreshError ? <p className="db-corruption-banner__error">{refreshError}</p> : null}
</section>
);
}