diff --git a/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.ts b/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.ts index 4b6525f0a9..ec93fbba2c 100644 --- a/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.ts +++ b/ui/app/(prowler)/lighthouse/_actions/lighthouse-v2.ts @@ -1,5 +1,6 @@ "use server"; +import { pollTaskUntilSettled } from "@/actions/task/poll"; import type { LighthouseV2Configuration, LighthouseV2ConfigurationInput, @@ -101,16 +102,51 @@ export async function deleteLighthouseV2Configuration( ); } +// Starts the backend connection-check task, polls it to completion (reusing the +// shared task poller), then returns the re-fetched configuration so the caller +// can render the authoritative `connected` / `connectionLastCheckedAt` status. export async function testLighthouseV2ConfigurationConnection( configId: string, -): Promise> { - return mutateSingle( - `/lighthouse/config/${encodeURIComponent(configId)}/connection`, - { method: "POST" }, - mapLighthouseV2Task, - "/lighthouse/config", - false, - ); +): Promise> { + try { + const response = await fetch( + buildApiUrl( + `/lighthouse/config/${encodeURIComponent(configId)}/connection`, + ), + { method: "POST", headers: await getAuthHeaders({ contentType: false }) }, + ); + const document = (await handleApiResponse( + response, + )) as JsonApiDocument; + if (isErrorDocument(document) || !document.data) { + return toErrorResult(document); + } + + const settled = await pollTaskUntilSettled( + mapLighthouseV2Task(document.data).id, + { + maxAttempts: 20, + delayMs: 3000, + }, + ); + if (!settled.ok) { + return { error: settled.error || "Connection test timed out." }; + } + + const configurations = await getLighthouseV2Configurations(); + if ("error" in configurations) { + return configurations; + } + const updated = configurations.data.find( + (config) => config.id === configId, + ); + if (!updated) { + return { error: "Configuration not found after connection test." }; + } + return { data: updated }; + } catch (error) { + return handleApiError(error); + } } export async function getLighthouseV2SupportedProviders(): Promise< diff --git a/ui/app/(prowler)/lighthouse/_components/config/configuration-form.tsx b/ui/app/(prowler)/lighthouse/_components/config/configuration-form.tsx index 69a04f69d9..946c63cf2f 100644 --- a/ui/app/(prowler)/lighthouse/_components/config/configuration-form.tsx +++ b/ui/app/(prowler)/lighthouse/_components/config/configuration-form.tsx @@ -6,13 +6,10 @@ import { KeyRound, Loader2, PlugZap, - RefreshCw, Save, - ShieldCheck, Sparkles, Trash2, } from "lucide-react"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; @@ -34,6 +31,7 @@ import { type LighthouseV2ConfigFormValues, trimToNullable, } from "@/app/(prowler)/lighthouse/_lib/config"; +import { formatLastChecked } from "@/app/(prowler)/lighthouse/_lib/format"; import { type LighthouseV2Configuration, type LighthouseV2ConfigurationInput, @@ -55,9 +53,7 @@ import { Textarea } from "@/components/shadcn/textarea/textarea"; import { cn } from "@/lib/utils"; import { ConfigurationSection } from "./configuration-section"; -import { ConnectionStatusPanel } from "./connection-status-panel"; import { CredentialFields } from "./credential-fields"; -import { ModelDetails } from "./model-details"; import { ProviderIcon } from "./provider-icon"; import { StatusBadge } from "./status-badge"; @@ -66,6 +62,7 @@ export function LighthouseV2ConfigurationForm({ models, onConfigurationDeleted, onConfigurationSaved, + onConfigurationTested, onFeedback, provider, }: { @@ -73,10 +70,10 @@ export function LighthouseV2ConfigurationForm({ models: LighthouseV2SupportedModel[]; onConfigurationDeleted: (configurationId: string) => void; onConfigurationSaved: (configuration: LighthouseV2Configuration) => void; + onConfigurationTested: (configuration: LighthouseV2Configuration) => void; onFeedback: (feedback: FeedbackState | null) => void; provider: LighthouseV2SupportedProvider; }) { - const router = useRouter(); const [saving, setSaving] = useState(false); const [testing, setTesting] = useState(false); const [deleting, setDeleting] = useState(false); @@ -91,10 +88,6 @@ export function LighthouseV2ConfigurationForm({ mode: "onSubmit", }); const businessContext = form.watch("businessContext"); - const selectedModel = form.watch("defaultModel"); - const selectedModelDetails = models.find( - (model) => model.id === selectedModel, - ); const status = getConnectionStatus(configuration); const handleSave = async (values: LighthouseV2ConfigFormValues) => { @@ -152,20 +145,14 @@ export function LighthouseV2ConfigurationForm({ if ("error" in result) { onFeedback({ - title: "Connection check failed to start", + title: "Connection check failed", description: result.error, variant: FEEDBACK_VARIANT.ERROR, }); return; } - onFeedback({ - title: "Connection check started.", - description: - "The backend is validating this provider. Refresh status when the task finishes.", - variant: FEEDBACK_VARIANT.INFO, - showRefreshStatus: true, - }); + onConfigurationTested(result.data); }; const handleDelete = async () => { @@ -191,7 +178,7 @@ export function LighthouseV2ConfigurationForm({ return (
-
+
+ + {formatLastChecked(configuration?.connectionLastCheckedAt)} +

{configuration @@ -222,15 +212,7 @@ export function LighthouseV2ConfigurationForm({ disabled={!configuration || testing} > {testing ? : } - Test connection - -

@@ -240,17 +222,6 @@ export function LighthouseV2ConfigurationForm({ onSubmit={form.handleSubmit(handleSave)} noValidate > - } - title="Connection" - description="Current backend check result for this provider." - > - - - } title="Credentials" @@ -262,7 +233,6 @@ export function LighthouseV2ConfigurationForm({ > @@ -300,7 +270,6 @@ export function LighthouseV2ConfigurationForm({ )} /> - -
+
{configuration ? "Saving updates may change chat behavior immediately." diff --git a/ui/app/(prowler)/lighthouse/_components/config/configuration-section.tsx b/ui/app/(prowler)/lighthouse/_components/config/configuration-section.tsx index 85c4ac7e76..15364206a5 100644 --- a/ui/app/(prowler)/lighthouse/_components/config/configuration-section.tsx +++ b/ui/app/(prowler)/lighthouse/_components/config/configuration-section.tsx @@ -12,7 +12,7 @@ export function ConfigurationSection({ title: string; }) { return ( -
+
{icon} diff --git a/ui/app/(prowler)/lighthouse/_components/config/connection-status-panel.tsx b/ui/app/(prowler)/lighthouse/_components/config/connection-status-panel.tsx deleted file mode 100644 index 0ab9529934..0000000000 --- a/ui/app/(prowler)/lighthouse/_components/config/connection-status-panel.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { AlertCircle, CheckCircle2, CircleDashed } from "lucide-react"; - -import { - CONNECTION_STATUS, - type ConnectionStatus, - getAlertVariant, - getConnectionStatusLabel, -} from "@/app/(prowler)/lighthouse/_lib/config"; -import { formatLastChecked } from "@/app/(prowler)/lighthouse/_lib/format"; -import { type LighthouseV2Configuration } from "@/app/(prowler)/lighthouse/_types"; -import { Alert, AlertDescription, AlertTitle } from "@/components/shadcn/alert"; - -export function ConnectionStatusPanel({ - configuration, - status, -}: { - configuration?: LighthouseV2Configuration; - status: ConnectionStatus; -}) { - const statusText = getConnectionStatusLabel(status); - const description = - status === CONNECTION_STATUS.CONNECTED - ? "Lighthouse can send messages with this provider." - : status === CONNECTION_STATUS.FAILED - ? "Connection failed. Review credentials and run another test." - : "Connection has not been tested yet."; - - return ( - - {status === CONNECTION_STATUS.CONNECTED ? ( - - ) : status === CONNECTION_STATUS.FAILED ? ( - - ) : ( - - )} - {statusText} - -

{description}

-

{formatLastChecked(configuration?.connectionLastCheckedAt)}

-
-
- ); -} diff --git a/ui/app/(prowler)/lighthouse/_components/config/credential-fields.tsx b/ui/app/(prowler)/lighthouse/_components/config/credential-fields.tsx index 758603943c..b8ae29443f 100644 --- a/ui/app/(prowler)/lighthouse/_components/config/credential-fields.tsx +++ b/ui/app/(prowler)/lighthouse/_components/config/credential-fields.tsx @@ -10,14 +10,12 @@ import { Input } from "@/components/shadcn/input/input"; export function CredentialFields({ errors, - hasConfiguration, provider, register, }: { errors: ReturnType< typeof useForm >["formState"]["errors"]; - hasConfiguration: boolean; provider: LighthouseV2ProviderType; register: ReturnType< typeof useForm @@ -25,12 +23,6 @@ export function CredentialFields({ }) { return (
- {hasConfiguration && ( -

- Leave blank to keep existing credentials. -

- )} - {(provider === LIGHTHOUSE_V2_PROVIDER_TYPE.OPENAI || provider === LIGHTHOUSE_V2_PROVIDER_TYPE.OPENAI_COMPATIBLE) && ( diff --git a/ui/app/(prowler)/lighthouse/_components/config/feedback-alert.tsx b/ui/app/(prowler)/lighthouse/_components/config/feedback-alert.tsx index 22c62acbb5..3de3f407eb 100644 --- a/ui/app/(prowler)/lighthouse/_components/config/feedback-alert.tsx +++ b/ui/app/(prowler)/lighthouse/_components/config/feedback-alert.tsx @@ -1,27 +1,24 @@ -import { AlertCircle, CheckCircle2, RefreshCw } from "lucide-react"; +import { AlertCircle, CheckCircle2, Info } from "lucide-react"; import { FEEDBACK_VARIANT, type FeedbackState, } from "@/app/(prowler)/lighthouse/_lib/config"; import { Alert, AlertDescription, AlertTitle } from "@/components/shadcn/alert"; -import { Button } from "@/components/shadcn/button/button"; export function ConfigFeedbackAlert({ feedback, onClose, - onRefreshStatus, }: { feedback: FeedbackState; onClose: () => void; - onRefreshStatus: () => void; }) { const Icon = feedback.variant === FEEDBACK_VARIANT.ERROR ? AlertCircle : feedback.variant === FEEDBACK_VARIANT.SUCCESS ? CheckCircle2 - : RefreshCw; + : Info; return ( @@ -29,18 +26,6 @@ export function ConfigFeedbackAlert({ {feedback.title} {feedback.description &&

{feedback.description}

} - {feedback.showRefreshStatus && ( - - )}
); diff --git a/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.test.tsx b/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.test.tsx index 278eebf747..e37fb4d373 100644 --- a/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.test.tsx +++ b/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.test.tsx @@ -13,21 +13,17 @@ import { LighthouseV2ConfigPage } from "./lighthouse-v2-config-page"; const { createConfigurationMock, deleteConfigurationMock, - refreshMock, testConnectionMock, updateConfigurationMock, } = vi.hoisted(() => ({ createConfigurationMock: vi.fn(), deleteConfigurationMock: vi.fn(), - refreshMock: vi.fn(), testConnectionMock: vi.fn(), updateConfigurationMock: vi.fn(), })); vi.mock("next/navigation", () => ({ - useRouter: () => ({ - refresh: refreshMock, - }), + useRouter: () => ({ refresh: vi.fn(), push: vi.fn() }), })); vi.mock("@/app/(prowler)/lighthouse/_actions", () => ({ @@ -90,20 +86,15 @@ describe("LighthouseV2ConfigPage", () => { beforeEach(() => { createConfigurationMock.mockReset(); deleteConfigurationMock.mockReset(); - refreshMock.mockReset(); testConnectionMock.mockReset(); updateConfigurationMock.mockReset(); createConfigurationMock.mockResolvedValue({ data: configurations[0] }); deleteConfigurationMock.mockResolvedValue({ data: true }); + // The action polls the task internally and resolves with the re-fetched + // configuration carrying the authoritative connection status. testConnectionMock.mockResolvedValue({ - data: { - id: "task-1", - name: "lighthouse-config-connection", - state: "PENDING", - insertedAt: "2026-06-24T10:01:00Z", - completedAt: null, - }, + data: { ...configurations[0], connected: true }, }); updateConfigurationMock.mockResolvedValue({ data: configurations[0] }); }); @@ -310,26 +301,25 @@ describe("LighthouseV2ConfigPage", () => { expect(updateConfigurationMock).not.toHaveBeenCalled(); }); - it("starts a connection test and exposes a refresh status action", async () => { + it("tests the connection and reports the resulting status", async () => { // Given const user = userEvent.setup(); + testConnectionMock.mockResolvedValue({ + data: { ...configurations[0], connected: false }, + }); renderPage(); // When await user.click(screen.getByRole("button", { name: /Test connection/i })); - // Then + // Then: the action is polled to completion and the resulting status shown await waitFor(() => expect(testConnectionMock).toHaveBeenCalledWith("config-openai"), ); + expect(await screen.findByText("Connection failed.")).toBeInTheDocument(); expect( - await screen.findByText("Connection check started."), - ).toBeInTheDocument(); - - await user.click( - screen.getAllByRole("button", { name: /Refresh status/i })[0], - ); - expect(refreshMock).toHaveBeenCalledTimes(1); + screen.queryByRole("button", { name: /Refresh status/i }), + ).not.toBeInTheDocument(); }); it("confirms before deleting an existing configuration", async () => { diff --git a/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.tsx b/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.tsx index 0532216e39..c5d6b42e79 100644 --- a/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.tsx +++ b/ui/app/(prowler)/lighthouse/_components/config/lighthouse-v2-config-page.tsx @@ -1,6 +1,5 @@ "use client"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import { @@ -36,7 +35,6 @@ export function LighthouseV2ConfigPage({ modelsByProvider, error, }: LighthouseV2ConfigPageProps) { - const router = useRouter(); const [localConfigurations, setLocalConfigurations] = useState(configurations); const [selectedProvider, setSelectedProvider] = @@ -63,13 +61,17 @@ export function LighthouseV2ConfigPage({ ? modelsByProvider[selectedProviderDefinition.id] : []; - const handleConfigurationSaved = ( - configuration: LighthouseV2Configuration, - ) => { + const upsertConfiguration = (configuration: LighthouseV2Configuration) => { setLocalConfigurations((current) => [ ...current.filter((config) => config.id !== configuration.id), configuration, ]); + }; + + const handleConfigurationSaved = ( + configuration: LighthouseV2Configuration, + ) => { + upsertConfiguration(configuration); setSelectedProvider(configuration.providerType); setFeedback({ title: "Configuration saved.", @@ -78,6 +80,26 @@ export function LighthouseV2ConfigPage({ }); }; + const handleConfigurationTested = ( + configuration: LighthouseV2Configuration, + ) => { + upsertConfiguration(configuration); + setFeedback( + configuration.connected + ? { + title: "Connection successful.", + description: "Lighthouse can send messages with this provider.", + variant: FEEDBACK_VARIANT.SUCCESS, + } + : { + title: "Connection failed.", + description: + "Review the credentials and test the connection again.", + variant: FEEDBACK_VARIANT.ERROR, + }, + ); + }; + const handleConfigurationDeleted = (configurationId: string) => { setLocalConfigurations((current) => current.filter((config) => config.id !== configurationId), @@ -105,7 +127,6 @@ export function LighthouseV2ConfigPage({
router.refresh()} onClose={() => setFeedback(null)} />
@@ -138,6 +159,7 @@ export function LighthouseV2ConfigPage({ provider={selectedProviderDefinition} onConfigurationSaved={handleConfigurationSaved} onConfigurationDeleted={handleConfigurationDeleted} + onConfigurationTested={handleConfigurationTested} onFeedback={setFeedback} />
diff --git a/ui/app/(prowler)/lighthouse/_components/config/model-details.tsx b/ui/app/(prowler)/lighthouse/_components/config/model-details.tsx deleted file mode 100644 index 8b53e81132..0000000000 --- a/ui/app/(prowler)/lighthouse/_components/config/model-details.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { CheckCircle2, CircleDashed } from "lucide-react"; - -import { formatTokenLimit } from "@/app/(prowler)/lighthouse/_lib/format"; -import { type LighthouseV2SupportedModel } from "@/app/(prowler)/lighthouse/_types"; - -export function ModelDetails({ - model, -}: { - model?: LighthouseV2SupportedModel; -}) { - if (!model) { - return ( -
-

- Select a model to see capabilities. -

-
- ); - } - - return ( -
- - - -
- Input tokens: {formatTokenLimit(model.maxInputTokens)} · Output tokens:{" "} - {formatTokenLimit(model.maxOutputTokens)} -
-
- ); -} - -function CapabilityItem({ - enabled, - label, -}: { - enabled: boolean | null; - label: string; -}) { - return ( -
- {enabled ? ( - - ) : ( - - )} - {label} -
- ); -} diff --git a/ui/app/(prowler)/lighthouse/_lib/config.ts b/ui/app/(prowler)/lighthouse/_lib/config.ts index 3712934f20..252d6fccb1 100644 --- a/ui/app/(prowler)/lighthouse/_lib/config.ts +++ b/ui/app/(prowler)/lighthouse/_lib/config.ts @@ -31,7 +31,6 @@ export interface FeedbackState { title: string; description?: string; variant: FeedbackVariant; - showRefreshStatus?: boolean; } const lighthouseV2ConfigFormSchemaBase = z.object({ @@ -174,15 +173,3 @@ export function getConnectionStatus( if (configuration?.connected === false) return CONNECTION_STATUS.FAILED; return CONNECTION_STATUS.NOT_TESTED; } - -export function getConnectionStatusLabel(status: ConnectionStatus): string { - if (status === CONNECTION_STATUS.CONNECTED) return "Connected"; - if (status === CONNECTION_STATUS.FAILED) return "Failed"; - return "Not tested"; -} - -export function getAlertVariant(status: ConnectionStatus) { - if (status === CONNECTION_STATUS.CONNECTED) return "success"; - if (status === CONNECTION_STATUS.FAILED) return "error"; - return "info"; -} diff --git a/ui/app/(prowler)/lighthouse/_lib/format.ts b/ui/app/(prowler)/lighthouse/_lib/format.ts index eea2003d8f..b299d74549 100644 --- a/ui/app/(prowler)/lighthouse/_lib/format.ts +++ b/ui/app/(prowler)/lighthouse/_lib/format.ts @@ -28,7 +28,3 @@ export function formatSessionAge(dateString: string): string { if (ageInDays === 0) return "Today"; return ageInDays === 1 ? "1 day" : `${ageInDays} days`; } - -export function formatTokenLimit(value: number | null): string { - return value === null ? "Unknown" : value.toLocaleString(); -}