import type { FeatureBundleState } from "@ashim/shared"; import { AlertCircle, Download, Loader2, RotateCcw } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { apiPost } from "@/lib/api"; import { useFeaturesStore } from "@/stores/features-store"; interface FeatureInstallPromptProps { bundle: FeatureBundleState; isAdmin: boolean; } interface ProgressState { percent: number; stage: string; } export function FeatureInstallPrompt({ bundle, isAdmin }: FeatureInstallPromptProps) { const [installing, setInstalling] = useState(false); const [progress, setProgress] = useState(null); const [error, setError] = useState(null); const eventSourceRef = useRef(null); const pollingRef = useRef | null>(null); const refresh = useFeaturesStore((s) => s.refresh); // If bundle is already installing on mount, show progress state immediately useEffect(() => { if (bundle.status === "installing") { setInstalling(true); setProgress(bundle.progress ?? { percent: 0, stage: "Installing..." }); } }, [bundle.status, bundle.progress]); // Cleanup on unmount useEffect(() => { return () => { eventSourceRef.current?.close(); if (pollingRef.current) clearInterval(pollingRef.current); }; }, []); function startPollingFallback() { if (pollingRef.current) return; pollingRef.current = setInterval(async () => { try { await refresh(); const updated = useFeaturesStore.getState().bundles.find((b) => b.id === bundle.id); if (!updated || updated.status !== "installing") { if (pollingRef.current) clearInterval(pollingRef.current); pollingRef.current = null; setInstalling(false); if (updated?.status === "error") { setError(updated.error ?? "Installation failed"); } else { setProgress(null); } } else if (updated.progress) { setProgress(updated.progress); } } catch { // ignore polling errors } }, 3000); } function listenToProgress(jobId: string) { const es = new EventSource(`/api/v1/jobs/${jobId}/progress`); eventSourceRef.current = es; es.onmessage = (event) => { try { const data = JSON.parse(event.data) as { phase: string; percent: number; stage: string; error?: string; }; if (data.phase === "complete") { es.close(); eventSourceRef.current = null; setInstalling(false); setProgress(null); refresh(); return; } if (data.phase === "failed") { es.close(); eventSourceRef.current = null; setInstalling(false); setError(data.error ?? "Installation failed"); return; } setProgress({ percent: data.percent, stage: data.stage }); } catch { // ignore malformed messages } }; es.onerror = () => { es.close(); eventSourceRef.current = null; startPollingFallback(); }; } async function handleInstall() { setInstalling(true); setError(null); setProgress({ percent: 0, stage: "Starting installation..." }); try { const result = await apiPost<{ jobId: string }>(`/v1/admin/features/${bundle.id}/install`); listenToProgress(result.jobId); } catch (err) { setInstalling(false); setError(err instanceof Error ? err.message : "Failed to start installation"); } } // Non-admin: show "not enabled" message if (!isAdmin) { return (

Feature Not Enabled

This feature is not enabled. Ask your administrator to enable it in Settings.

); } // Admin: show install prompt return (

{bundle.name}

{bundle.description}

This feature requires an additional download (~{bundle.estimatedSize})

{/* Error banner */} {error && (
{error}
)} {/* Progress bar */} {installing && progress && (
{progress.stage}
)} {/* Install button (hidden when installing or showing error) */} {!installing && !error && ( )}
); }