Files
office_translator/frontend/src/app/dashboard/services/page.tsx
sepehr ce8e150a61 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>
2026-05-10 11:43:28 +02:00

153 lines
5.7 KiB
TypeScript

'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>
);
}