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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user