Files
fusion/packages/dashboard/app/components/EngineStatusBanner.tsx
gsxdsm e46ea00ff5 FN-6827: add engine status banner
Add project-scoped engine connectivity remediation to the dashboard.

- Add engine status/start API helpers and server routes that can resume paused projects or start missing engines.
- Render a project banner with dashboard-only guidance, polling, disabled starting state, localized copy, and focused tests.
- Document the engine status banner behavior and add a changeset for the published CLI package.

Files changed:
 .changeset/fn-6827-engine-disconnected-banner.md   |   7 +
 docs/dashboard-guide.md                            |   8 +
 packages/dashboard/app/api/legacy.ts               |  20 +++
 .../app/components/EngineStatusBanner.css          |  87 +++++++++++
 .../app/components/EngineStatusBanner.tsx          |  62 ++++++++
 .../__tests__/EngineStatusBanner.test.tsx          | 145 +++++++++++++++++
 .../app/components/dashboard/DashboardBanners.tsx  |   3 +
 .../app/hooks/__tests__/useEngineStatus.test.ts    | 171 +++++++++++++++++++++
 packages/dashboard/app/hooks/useEngineStatus.ts    | 111 +++++++++++++
 packages/dashboard/src/__tests__/server.test.ts    | 129 ++++++++++++++++
 packages/dashboard/src/server.ts                   |  89 +++++++++++
 packages/i18n/locales/en/app.json                  |  10 ++
 packages/i18n/locales/es/app.json                  |  10 ++
 packages/i18n/locales/fr/app.json                  |  10 ++
 packages/i18n/locales/ko/app.json                  |  10 ++
 packages/i18n/locales/zh-CN/app.json               |  10 ++
 packages/i18n/locales/zh-TW/app.json               |  10 ++
 17 files changed, 892 insertions(+)

Fusion-Task-Id: FN-6827

Fusion-Task-Lineage: 56821ca3-04c8-4674-9400-3639f10e463d
2026-06-25 08:35:44 -07:00

63 lines
3.0 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Loader2 } from "lucide-react";
import { useEngineStatus } from "../hooks/useEngineStatus";
import "./EngineStatusBanner.css";
interface EngineStatusBannerProps {
projectId: string;
}
/*
* FNXC:EngineStatusBanner 2026-06-22-00:00:
* The project banner stack needs a single visible remediation when the dashboard is loaded but the current project's engine is absent. Hide the entire component once `connected` is true so no empty wrapper, button shell, or stale aria surface remains in the DOM.
*/
export function EngineStatusBanner({ projectId }: EngineStatusBannerProps) {
const { t } = useTranslation("app");
const { status, canStart, starting, error, start } = useEngineStatus(projectId);
if (!status || status.connected) return null;
const isDashboardOnly = status.reason === "dashboard-only" || status.reason === "unreachable";
const statusDotClass = starting ? "status-dot status-dot--connecting" : "status-dot status-dot--error";
const body = canStart
? t("engineBanner.body", "The engine for this project is not running, so task automation and live updates may be paused. Start it now to reconnect the dashboard.")
: t("engineBanner.dashboardOnly", "This dashboard cannot start engines from the current process. Run `fn serve` for this project to enable task execution and live automation.");
return (
<section className="engine-status-banner" role="status" aria-live="polite" data-testid="engine-status-banner">
<div className="engine-status-banner__indicator" aria-hidden="true">
<span className={statusDotClass} />
</div>
<div className="engine-status-banner__content">
<div className="engine-status-banner__title">{t("engineBanner.title", "Project engine is not connected")}</div>
<p className="engine-status-banner__body">
{isDashboardOnly ? (
<>
{t("engineBanner.dashboardOnlyPrefix", "This dashboard cannot start engines from the current process. Run")} <code>fn serve</code> {t("engineBanner.dashboardOnlySuffix", "for this project to enable task execution and live automation.")}
</>
) : body}
</p>
{error && (
<p className="engine-status-banner__error" role="alert">
{t("engineBanner.error", "Start failed: {{message}}", { message: error })}
</p>
)}
</div>
<div className="engine-status-banner__actions">
{canStart ? (
<button
type="button"
className="btn btn-primary btn-sm engine-status-banner__start"
onClick={() => void start()}
disabled={starting}
data-testid="engine-status-start-button"
>
{starting ? <Loader2 className="spinner" aria-hidden="true" /> : null}
{starting ? t("engineBanner.starting", "Starting…") : t("engineBanner.startCta", "Start engine")}
</button>
) : null}
</div>
</section>
);
}