diff --git a/backend/src/app.ts b/backend/src/app.ts index 1a21ee28..5c7ba9d5 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -86,10 +86,11 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc const authenticate = authPreHandler(config.authMode); app.addHook("preHandler", async (req, reply) => { // Process readiness is intentionally unauthenticated for local container/proxy probes. - if (req.url === "/health") return; + if (req.url === "/health" || req.url === "/health/dwh") return; return authenticate(req, reply); }); app.get("/health", async () => ({ status: "ok" })); + app.get("/health/dwh", async () => tht.dbPing()); app.get("/me", async (req) => getPrincipal(req)); sessionRoutes(app, { mgr, tht: tht as ThtRunner, hub, getSettings, readiness, dwhPrecheck: config.dwhPrecheck, diff --git a/backend/src/routes/sessions.ts b/backend/src/routes/sessions.ts index 6f228041..fd660456 100644 --- a/backend/src/routes/sessions.ts +++ b/backend/src/routes/sessions.ts @@ -14,7 +14,7 @@ const READINESS_FAILURE_MESSAGE = const RESUME_FAILURE_MESSAGE = "Session could not be resumed. Check configuration and connectivity, then try again."; const DWH_UNREACHABLE_MESSAGE = - "Cannot start a session: the data warehouse is unreachable. Check the VPN connection and try again."; + "Cannot start a session: the database is unreachable. Check the VPN connection and try again."; export function sessionRoutes( app: FastifyInstance, diff --git a/backend/src/tht/tht-runner.ts b/backend/src/tht/tht-runner.ts index 96f42cc7..ab416e8c 100644 --- a/backend/src/tht/tht-runner.ts +++ b/backend/src/tht/tht-runner.ts @@ -154,7 +154,7 @@ export class ThtRunner { * cleanly. Timed out to keep POST /sessions responsive when the host hangs. */ async dbPing(workspace?: string): Promise<{ ok: boolean; detail: string }> { - const { code, stdout, stderr } = await this.run(["db", "ping"], workspace, 10_000); + const { code, stdout, stderr } = await this.run(["db", "ping"], workspace, 5_000); if (code === 0) return { ok: true, detail: stdout.trim() }; return { ok: false, detail: (stderr || stdout).trim() }; } diff --git a/frontend/src/api/sessions.ts b/frontend/src/api/sessions.ts index eabb2874..2067ecfd 100644 --- a/frontend/src/api/sessions.ts +++ b/frontend/src/api/sessions.ts @@ -53,3 +53,15 @@ export const deleteSession = (id: string) => export const getSessionDocuments = (id: string) => apiFetch(`/sessions/${id}/documents`); + +export async function checkDwhHealth(): Promise<{ ok: boolean }> { + const timeout = new Promise<{ ok: false }>((r) => setTimeout(() => r({ ok: false }), 5000)); + try { + return await Promise.race([ + apiFetch<{ ok: boolean; detail: string }>("/health/dwh"), + timeout, + ]); + } catch { + return { ok: false }; + } +} diff --git a/frontend/src/shell/AppShell.new-session.test.tsx b/frontend/src/shell/AppShell.new-session.test.tsx index 57550309..e1020410 100644 --- a/frontend/src/shell/AppShell.new-session.test.tsx +++ b/frontend/src/shell/AppShell.new-session.test.tsx @@ -100,7 +100,7 @@ test("a DWH-unreachable precheck shows a specific alert and preserves the questi http.post("http://localhost:8787/sessions", () => HttpResponse.json( { - error: "Cannot start a session: the data warehouse is unreachable. Check the VPN connection and try again.", + error: "Cannot start a session: the database is unreachable. Check the VPN connection and try again.", code: "dwh_unreachable", }, { status: 503 }, @@ -113,7 +113,7 @@ test("a DWH-unreachable precheck shows a specific alert and preserves the questi await userEvent.click(screen.getByRole("button", { name: /send/i })); // Specific alert, not the generic "failed to create session" hint. - expect(await screen.findByText(/data warehouse is unreachable/i)).toBeInTheDocument(); + expect(await screen.findByText(/database is unreachable/i)).toBeInTheDocument(); expect(screen.queryByText(/failed to create session/i)).not.toBeInTheDocument(); // No session was created: landing view stays and the question is kept for retry. expect(screen.getByText(/type your question/i)).toBeInTheDocument(); diff --git a/frontend/src/shell/AppShell.tsx b/frontend/src/shell/AppShell.tsx index 68a83a9e..385ebe9d 100644 --- a/frontend/src/shell/AppShell.tsx +++ b/frontend/src/shell/AppShell.tsx @@ -20,7 +20,11 @@ import { toast } from "sonner"; import { closeSession, getMe, listSessions, resumeSession, getSession, renameSession, setSessionGroup, archiveSession, unarchiveSession, deleteSession, prewarmRuntime, + checkDwhHealth, } from "../api/sessions"; +import { + Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, +} from "../components/ui/dialog"; import type { Principal, SessionScope, SessionSummary } from "../api/types"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useEffect, useMemo, useRef, useState } from "react"; @@ -89,6 +93,19 @@ export function AppShell() { const [stopConfirm, setStopConfirm] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState>({}); const [renameGroupTarget, setRenameGroupTarget] = useState(null); + const [dwhDown, setDwhDown] = useState(false); + const [dwhChecking, setDwhChecking] = useState(true); + const [dwhCheckEpoch, setDwhCheckEpoch] = useState(0); + useEffect(() => { + let cancelled = false; + setDwhChecking(true); + checkDwhHealth().then((r) => { + if (cancelled) return; + setDwhDown(!r.ok); + setDwhChecking(false); + }); + return () => { cancelled = true; }; + }, [dwhCheckEpoch]); const groups = useMemo( () => [...new Set(sessions.map((s) => s.group).filter((g): g is string => !!g))].sort(), @@ -759,6 +776,26 @@ export function AppShell() { }} /> )} + {dwhDown && ( + {}}> + + + Connection unavailable + + The database is unreachable. Check the VPN connection and try again. + + + + + + + + )} ); } @@ -770,7 +807,7 @@ function EmptyState() { ThothII

- Build datamarts from your data warehouse through a guided, + Build datamarts from your database through a guided, human-in-the-loop workflow. Type your question in the box below to begin a session.

diff --git a/frontend/src/test/msw.ts b/frontend/src/test/msw.ts index 818dc20c..e9a9111c 100644 --- a/frontend/src/test/msw.ts +++ b/frontend/src/test/msw.ts @@ -1,2 +1,5 @@ import { setupServer } from "msw/node"; -export const server = setupServer(); +import { http, HttpResponse } from "msw"; +export const server = setupServer( + http.get("http://localhost:8787/health/dwh", () => HttpResponse.json({ ok: true, detail: "ok" })), +);