Replace Puppeteer with Next.js ImageResponse for previews

- Remove puppeteer (2.9GB) in favor of built-in ImageResponse (0 deps)
- Preview endpoint generates styled deployment card as PNG
- Shows app name, status, branch, commit, duration, domain
- Rename route.ts to route.tsx for JSX support
- Simplify dashboard to use image URL directly
This commit is contained in:
Alejandro Gutiérrez
2026-02-06 19:41:40 +01:00
parent 91624fd6de
commit 9b9226b954
3 changed files with 246 additions and 100 deletions

View File

@@ -47,13 +47,6 @@ interface StatsResponse {
timestamp: string;
}
interface PreviewResponse {
exists: boolean;
url?: string;
expiresIn?: number;
message?: string;
hint?: string;
}
interface DeploymentDashboardProps {
deployment: Deployment;
@@ -484,12 +477,8 @@ export function DeploymentDashboard({ deployment }: DeploymentDashboardProps) {
{ refreshInterval: 10000 }
);
// Preview image (only fetch once, no refresh needed)
const { data: preview, isLoading: previewLoading } = useSWR<PreviewResponse>(
`/api/deployments/${deployment.deployment_uuid}/preview`,
fetcher,
{ revalidateOnFocus: false }
);
// Preview image URL (ImageResponse generates PNG directly)
const previewUrl = `/api/deployments/${deployment.deployment_uuid}/preview`;
const coolifyUrl = `${clientConfig.coolifyUrl}/project/${clientConfig.coolifyProjectUuid}/production/application/${deployment.application_uuid}/deployment/${deployment.deployment_uuid}`;
@@ -645,40 +634,14 @@ export function DeploymentDashboard({ deployment }: DeploymentDashboardProps) {
{/* Main content: Preview + Metadata Grid */}
<div className="p-6">
<div className="flex gap-6">
{/* Preview thumbnail (left side) */}
{/* Preview thumbnail (left side) - generated via ImageResponse */}
<div className="flex-shrink-0 w-80">
<div className="aspect-[16/10] bg-slate-900 dark:bg-stone-950 rounded-lg flex items-center justify-center overflow-hidden border border-slate-200 dark:border-stone-700 relative">
{previewLoading ? (
<div className="text-center">
<Icon name="loader" size={32} className="text-slate-600 dark:text-stone-600 mx-auto mb-2 animate-spin" />
<span className="text-xs text-slate-500 dark:text-stone-500">Loading preview...</span>
</div>
) : preview?.exists && preview.url ? (
<img
src={preview.url}
alt={`Preview of ${deployment.application_name}`}
className="w-full h-full object-cover"
onError={(e) => {
// Hide image on error and show placeholder
e.currentTarget.style.display = 'none';
e.currentTarget.nextElementSibling?.classList.remove('hidden');
}}
/>
) : (
<div className="text-center">
<Icon name="image" size={48} className="text-slate-600 dark:text-stone-600 mx-auto mb-2" />
<span className="text-xs text-slate-500 dark:text-stone-500">
{deployment.status === 'in_progress' ? 'Preview after deploy' : 'No preview'}
</span>
</div>
)}
{/* Fallback placeholder (hidden by default, shown on image error) */}
<div className="hidden absolute inset-0 flex items-center justify-center bg-slate-900 dark:bg-stone-950">
<div className="text-center">
<Icon name="image" size={48} className="text-slate-600 dark:text-stone-600 mx-auto mb-2" />
<span className="text-xs text-slate-500 dark:text-stone-500">Preview unavailable</span>
</div>
</div>
<div className="aspect-[16/10] rounded-lg overflow-hidden border border-slate-200 dark:border-stone-700">
<img
src={previewUrl}
alt={`Preview of ${deployment.application_name}`}
className="w-full h-full object-cover"
/>
</div>
</div>