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:
152
frontend/src/app/dashboard/services/page.tsx
Normal file
152
frontend/src/app/dashboard/services/page.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Zap, CheckCircle2, Lock, Loader2, Globe, Brain } from 'lucide-react';
|
||||
import { API_BASE } from '@/lib/config';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
const FALLBACK_PROVIDERS = [
|
||||
{ id: "google", label: "Google Traduction", description: "Traduction rapide, 130+ langues", mode: "classic" as const },
|
||||
];
|
||||
|
||||
interface AvailableProvider {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
mode: "classic" | "llm";
|
||||
model?: string;
|
||||
}
|
||||
|
||||
export default function TranslationServicesPage() {
|
||||
const { t } = useI18n();
|
||||
const [providers, setProviders] = useState<AvailableProvider[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchProviders = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem("token");
|
||||
const headers: Record<string, string> = {};
|
||||
if (token) headers["Authorization"] = `Bearer ${token}`;
|
||||
const res = await fetch(`${API_BASE}/api/v1/providers/available`, { headers });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const list = data.providers || [];
|
||||
setProviders(list.length > 0 ? list : FALLBACK_PROVIDERS);
|
||||
} else {
|
||||
setProviders(FALLBACK_PROVIDERS);
|
||||
}
|
||||
} catch {
|
||||
setProviders(FALLBACK_PROVIDERS);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchProviders();
|
||||
}, []);
|
||||
|
||||
const classicProviders = providers.filter((p) => p.mode === "classic");
|
||||
const llmProviders = providers.filter((p) => p.mode === "llm");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 lg:p-8 max-w-2xl">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Zap className="h-5 w-5 text-primary" />
|
||||
<h1 className="text-2xl font-bold text-foreground">{t('services.title')}</h1>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('services.subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
<span>{t('services.loading')}</span>
|
||||
</div>
|
||||
) : providers.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
{t('services.noProviders')}
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{classicProviders.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Globe className="size-4 text-muted-foreground" />
|
||||
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
{t('services.classic')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{classicProviders.map((p) => (
|
||||
<Card key={p.id}>
|
||||
<CardContent className="flex items-center justify-between py-4 px-5">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{p.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{p.description}</p>
|
||||
</div>
|
||||
<Badge variant="outline" className="border-emerald-500/50 text-emerald-600 gap-1">
|
||||
<CheckCircle2 className="size-3" />{t('services.available')}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{llmProviders.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Brain className="size-4 text-muted-foreground" />
|
||||
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
|
||||
{t('services.llmPro')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{llmProviders.map((p) => (
|
||||
<Card key={p.id}>
|
||||
<CardContent className="flex items-center justify-between py-4 px-5">
|
||||
<div>
|
||||
<p className="font-medium text-foreground">{p.label}</p>
|
||||
<p className="text-xs text-muted-foreground">{p.description}</p>
|
||||
{p.model && (
|
||||
<p className="mt-1 text-[10px] font-mono text-muted-foreground/80">
|
||||
{t('services.model')}: {p.model}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="border-emerald-500/50 text-emerald-600 gap-1">
|
||||
<CheckCircle2 className="size-3" />{t('services.available')}
|
||||
</Badge>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card className="border-amber-200 dark:border-amber-500/20 bg-amber-50 dark:bg-amber-500/5">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm text-amber-800 dark:text-amber-400 flex items-center gap-2">
|
||||
<Lock className="size-4" />
|
||||
{t('services.adminOnly.title')}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription className="text-amber-700 dark:text-amber-400/80 text-xs">
|
||||
{t('services.adminOnly.desc')}
|
||||
</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user