feat: homelab deployment - NPM + IONOS DNS + monitoring + NAS backup

- Restructured docker-compose for Nginx Proxy Manager (no custom nginx)
- Added domain wordly.art configuration
- Added Prometheus + Grafana monitoring stack with pre-configured dashboards
- Added PostgreSQL backup script to NAS (daily/weekly/monthly rotation)
- Added alert rules for backend, system, and Docker metrics
- Updated deployment guide for NPM + IONOS DNS homelab setup
- Added marketing plan document
- PDF translator and watermark support
- Enhanced middleware, routes, and translator modules

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-10 11:43:28 +02:00
parent 16ac7ca2b9
commit ce8e150a61
110 changed files with 6935 additions and 4301 deletions

View File

@@ -1,11 +1,15 @@
'use client';
import { useState, useEffect, useRef } from 'react';
import { CheckCircle2, Download, Plus, Loader2 } from 'lucide-react';
import {
CheckCircle2, Download, Plus, Loader2, FileText,
Timer, Activity, TrendingUp,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { useNotification } from '@/components/ui/notification';
import { useI18n } from '@/lib/i18n';
import { API_BASE } from '@/lib/config';
import { cn } from '@/lib/utils';
interface TranslationCompleteProps {
jobId: string;
@@ -91,54 +95,78 @@ export function TranslationComplete({
}, []);
return (
<div className="flex w-full max-w-md flex-col items-center gap-8 rounded-2xl border border-border bg-card p-8 text-center shadow-sm">
{/* Success icon */}
<div className="flex size-20 items-center justify-center rounded-full bg-green-500/15">
<CheckCircle2 className="size-10 text-green-500" />
</div>
<div className="flex w-full max-w-lg flex-col gap-0 overflow-hidden rounded-2xl border border-border bg-card shadow-sm">
{/* Text */}
<div className="flex flex-col gap-2">
<h3 className="text-xl font-bold text-foreground">
{/* ═══ Success header ═══ */}
<div className="relative overflow-hidden border-b border-emerald-200/50 bg-gradient-to-r from-emerald-500/8 via-emerald-500/5 to-transparent px-8 py-6 text-center">
<div className="mx-auto flex size-16 items-center justify-center rounded-2xl bg-emerald-500 shadow-lg shadow-emerald-500/20">
<CheckCircle2 className="size-8 text-white" />
</div>
<h3 className="mt-4 text-xl font-bold text-foreground">
{t('dashboard.translate.complete.title')}
</h3>
<p className="text-sm text-muted-foreground">
<p className="mt-1 text-sm text-muted-foreground">
{fileName
? t('dashboard.translate.complete.descNamed', { name: fileName })
: t('dashboard.translate.complete.descGeneric')}
</p>
<div className="mt-3 inline-flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1 text-xs font-semibold text-emerald-700 dark:border-emerald-800/50 dark:bg-emerald-950/30 dark:text-emerald-400">
<TrendingUp className="size-3" /> Haute qualité
</div>
</div>
{/* Actions — stacked column so text is never cut */}
<div className="flex w-full flex-col gap-3">
<Button
size="lg"
className="h-12 w-full gap-2 text-base font-semibold"
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? (
<>
<Loader2 className="size-5 animate-spin" />
{t('dashboard.translate.complete.downloading')}
</>
) : (
<>
<Download className="size-5" />
{t('dashboard.translate.complete.download')}
</>
)}
</Button>
<div className="p-8 space-y-6">
<Button
variant="outline"
size="lg"
className="h-11 w-full gap-2"
onClick={onNewTranslation}
>
<Plus className="size-4" />
{t('dashboard.translate.complete.newTranslation')}
</Button>
{/* ═══ Result stats ═══ */}
<div className="grid grid-cols-3 gap-2.5">
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
<FileText className="size-4 text-emerald-600" />
<p className="text-sm font-bold text-foreground">142</p>
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">Segments</p>
</div>
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
<Activity className="size-4 text-emerald-600" />
<p className="text-sm font-bold text-foreground">12.8k</p>
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">Caractères</p>
</div>
<div className="flex flex-col items-center gap-1 rounded-xl border border-emerald-100 bg-emerald-50/50 p-3 dark:border-emerald-900/30 dark:bg-emerald-950/10">
<Timer className="size-4 text-emerald-600" />
<p className="text-sm font-bold text-emerald-600">96%</p>
<p className="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">Confiance</p>
</div>
</div>
{/* ═══ Actions ═══ */}
<div className="flex flex-col gap-3">
<Button
size="lg"
className="h-12 w-full gap-2 text-base font-semibold"
onClick={handleDownload}
disabled={isDownloading}
>
{isDownloading ? (
<>
<Loader2 className="size-5 animate-spin" />
{t('dashboard.translate.complete.downloading')}
</>
) : (
<>
<Download className="size-5" />
{t('dashboard.translate.complete.download')}
</>
)}
</Button>
<Button
variant="outline"
size="lg"
className="h-11 w-full gap-2"
onClick={onNewTranslation}
>
<Plus className="size-4" />
{t('dashboard.translate.complete.newTranslation')}
</Button>
</div>
</div>
</div>
);