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

@@ -0,0 +1,195 @@
'use client';
import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { useTranslationStore } from '@/lib/store';
import { API_BASE } from '@/lib/config';
import { useI18n } from '@/lib/i18n';
import { Save, Loader2, Brain, BookOpen, Sparkles, Trash2, Zap, Crown, Lock } from 'lucide-react';
import Link from 'next/link';
import { cn } from '@/lib/utils';
const PRESETS = [
{ key: 'hvac', icon: '🔧', title: 'HVAC / Génie climatique', desc: 'Thermique, ventilation, climatisation' },
{ key: 'construction', icon: '🏗️', title: 'BTP / Construction', desc: 'Gros œuvre, second œuvre, normes' },
{ key: 'it', icon: '💻', title: 'IT / Logiciel', desc: 'Développement, infrastructure, DevOps' },
{ key: 'legal', icon: '⚖️', title: 'Juridique / Contrats', desc: 'Droit des affaires, contentieux' },
{ key: 'medical', icon: '🏥', title: 'Médical / Santé', desc: 'Pharmacologie, chirurgie, diagnostic' },
{ key: 'finance', icon: '📊', title: 'Finance / Comptabilité', desc: 'IFRS, bilans, fiscalité' },
{ key: 'marketing', icon: '📢', title: 'Marketing / Publicité', desc: 'Digital, branding, analytics' },
{ key: 'automotive', icon: '🚗', title: 'Automobile', desc: 'Motorisation, ADAS, homologation' },
];
export default function ContextGlossaryPage() {
const { t } = useI18n();
const { settings, updateSettings, applyPreset, clearContext } = useTranslationStore();
const [isSaving, setIsSaving] = useState(false);
const [isPro, setIsPro] = useState(false);
const [localSettings, setLocalSettings] = useState({
systemPrompt: settings.systemPrompt,
glossary: settings.glossary,
});
useEffect(() => {
setLocalSettings({ systemPrompt: settings.systemPrompt, glossary: settings.glossary });
}, [settings]);
useEffect(() => {
const checkTier = async () => {
const isProTier = (user: any) => ['pro', 'business', 'enterprise'].includes(user?.plan ?? user?.tier ?? '');
const userStr = localStorage.getItem('user');
if (userStr) {
try {
const user = JSON.parse(userStr);
if (user?.plan || user?.tier) { setIsPro(isProTier(user)); return; }
} catch { /* continue */ }
}
try {
const token = localStorage.getItem('token');
if (!token) return;
const res = await fetch(`${API_BASE}/api/v1/auth/me`, { headers: { Authorization: `Bearer ${token}` } });
if (res.ok) {
const result = await res.json();
const user = result.data;
setIsPro(isProTier(user));
localStorage.setItem('user', JSON.stringify(user));
}
} catch { /* ignore */ }
};
checkTier();
}, []);
const handleSave = async () => {
setIsSaving(true);
try {
updateSettings(localSettings);
await new Promise(resolve => setTimeout(resolve, 500));
} finally { setIsSaving(false); }
};
const handleApplyPreset = (key: string) => {
applyPreset(key);
setTimeout(() => {
setLocalSettings({
systemPrompt: useTranslationStore.getState().settings.systemPrompt,
glossary: useTranslationStore.getState().settings.glossary,
});
}, 0);
};
const handleClear = () => {
clearContext();
setLocalSettings({ systemPrompt: '', glossary: '' });
};
if (!isPro) {
return (
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-6 p-6">
<div className="flex items-center justify-center w-20 h-20 rounded-2xl bg-violet-100 dark:bg-violet-900/30">
<Crown className="w-10 h-10 text-violet-600 dark:text-violet-400" />
</div>
<div className="text-center space-y-2 max-w-md">
<h1 className="text-2xl font-bold text-foreground">{t('context.proTitle')}</h1>
<p className="text-muted-foreground">
{t('context.proDesc')}
</p>
</div>
<Button asChild size="lg">
<Link href="/pricing">
<Crown className="w-4 h-4 me-2" /> {t('context.viewPlans')}
</Link>
</Button>
</div>
);
}
return (
<div className="flex flex-col gap-6 p-6 lg:p-8 max-w-3xl">
<div>
<h1 className="text-2xl font-bold text-foreground">{t('context.title')}</h1>
<p className="text-sm text-muted-foreground mt-1">
{t('context.subtitle')}
</p>
</div>
{/* Presets */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Zap className="w-4 h-4 text-accent" /> {t('context.presets.title')}
</CardTitle>
<CardDescription>{t('context.presets.desc')}</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
{PRESETS.map(p => (
<button
key={p.key}
onClick={() => handleApplyPreset(p.key)}
className="flex flex-col items-center gap-1 p-3 rounded-xl border border-border text-center transition-colors hover:border-primary/40 hover:bg-primary/5"
>
<span className="text-2xl">{p.icon}</span>
<span className="text-xs font-medium text-foreground leading-tight">{p.title}</span>
<span className="text-[10px] text-muted-foreground leading-tight">{p.desc}</span>
</button>
))}
</div>
</CardContent>
</Card>
{/* System Prompt */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<Brain className="w-4 h-4 text-primary" /> {t('context.instructions.title')}
</CardTitle>
<CardDescription>{t('context.instructions.desc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Textarea
value={localSettings.systemPrompt}
onChange={e => setLocalSettings({ ...localSettings, systemPrompt: e.target.value })}
placeholder={t('context.instructions.placeholder')}
className="min-h-[140px] resize-y"
/>
</CardContent>
</Card>
{/* Glossary */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base flex items-center gap-2">
<BookOpen className="w-4 h-4 text-emerald-500" /> {t('context.glossary.title')}
</CardTitle>
<CardDescription>{t('context.glossary.desc')}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Textarea
value={localSettings.glossary}
onChange={e => setLocalSettings({ ...localSettings, glossary: e.target.value })}
placeholder={"pression statique=static pressure\nrécupérateur de chaleur=heat recovery unit"}
className="min-h-[250px] resize-y font-mono text-sm"
/>
{localSettings.glossary && (
<p className="text-xs text-muted-foreground">
{localSettings.glossary.split('\n').filter(l => l.includes('=')).length} {t('context.glossary.terms')}
</p>
)}
</CardContent>
</Card>
{/* Actions */}
<div className="flex gap-3 justify-end">
<Button variant="ghost" onClick={handleClear} className="text-destructive hover:text-destructive/80 hover:bg-destructive/10">
<Trash2 className="me-1.5 h-4 w-4" /> {t('context.clearAll')}
</Button>
<Button onClick={handleSave} disabled={isSaving}>
{isSaving ? <><Loader2 className="me-1.5 h-4 w-4 animate-spin" />{t('context.saving')}</> : <><Save className="me-1.5 h-4 w-4" />{t('context.save')}</>}
</Button>
</div>
</div>
);
}