Add S3/MinIO storage for deployment previews

- Add S3 client helper (src/lib/s3.ts) with upload/download functions
- Add /api/deployments/[uuid]/preview endpoint for presigned URLs
- Update DeploymentDashboard to fetch and display preview images
- Install @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner

Storage: MinIO bucket nuc-portal-previews with dedicated service account

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-06 18:11:50 +01:00
parent efc7a8392b
commit 91624fd6de
5 changed files with 2022 additions and 4 deletions

View File

@@ -47,6 +47,14 @@ interface StatsResponse {
timestamp: string;
}
interface PreviewResponse {
exists: boolean;
url?: string;
expiresIn?: number;
message?: string;
hint?: string;
}
interface DeploymentDashboardProps {
deployment: Deployment;
}
@@ -476,6 +484,13 @@ 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 }
);
const coolifyUrl = `${clientConfig.coolifyUrl}/project/${clientConfig.coolifyProjectUuid}/production/application/${deployment.application_uuid}/deployment/${deployment.deployment_uuid}`;
// Dozzle URL for runtime logs (uses IP since no domain configured)
@@ -632,10 +647,37 @@ export function DeploymentDashboard({ deployment }: DeploymentDashboardProps) {
<div className="flex gap-6">
{/* Preview thumbnail (left side) */}
<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">
<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</span>
<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>
</div>