feat: production deployment - full update with providers, admin, glossaries, pricing, tests

Major changes across backend, frontend, infrastructure:
- Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud)
- Admin panel: user management, pricing, settings
- Glossary system with CSV import/export
- Subscription and tier quota management
- Security hardening (rate limiting, API key auth, path traversal fixes)
- Docker compose for dev, prod, and IONOS deployment
- Alembic migrations for new tables
- Frontend: dashboard, pricing page, landing page, i18n (en/fr)
- Test suite and verification scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sepehr Ramezani
2026-04-25 15:01:47 +02:00
parent 2ba4fedfc8
commit 26bd096a06
1178 changed files with 136435 additions and 3047 deletions

View File

@@ -6,6 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { useUser } from '@/app/dashboard/useUser';
import { useI18n } from '@/lib/i18n';
import { useGlossaries, useGlossary } from './useGlossaries';
import type { Glossary, GlossaryTermInput, GlossaryListItem } from './types';
import { ProUpgradePrompt } from './ProUpgradePrompt';
@@ -16,6 +17,7 @@ import { DeleteGlossaryDialog } from './DeleteGlossaryDialog';
import { useToast } from '@/components/ui/toast';
export default function GlossariesPage() {
const { t } = useI18n();
const { data: user, isLoading: isLoadingUser } = useUser();
const {
glossaries,
@@ -24,9 +26,11 @@ export default function GlossariesPage() {
isCreating,
isUpdating,
isDeleting,
isImportingTemplate,
createGlossary,
updateGlossary,
deleteGlossary,
importTemplate,
} = useGlossaries();
const { toast } = useToast();
@@ -62,14 +66,34 @@ export default function GlossariesPage() {
await createGlossary(data);
setCreateDialogOpen(false);
toast({
title: 'Glossary created',
description: `"${data.name}" has been created successfully.`,
title: t('glossaries.toast.created'),
description: t('glossaries.toast.createdDesc', { name: data.name }),
});
} catch (error) {
toast({
variant: 'destructive',
title: 'Error',
description: 'Failed to create glossary. Please try again.',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorCreate'),
});
throw error;
}
};
const handleImportTemplate = async (templateId: string, name?: string) => {
try {
await importTemplate(templateId, name);
setCreateDialogOpen(false);
toast({
title: t('glossaries.toast.imported'),
description: name
? t('glossaries.toast.importedDesc', { name })
: t('glossaries.toast.importedDesc', { name: templateId }),
});
} catch (error) {
toast({
variant: 'destructive',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorImport'),
});
throw error;
}
@@ -81,14 +105,14 @@ export default function GlossariesPage() {
setEditDialogOpen(false);
setSelectedGlossary(null);
toast({
title: 'Glossary updated',
description: `"${data.name}" has been updated successfully.`,
title: t('glossaries.toast.updated'),
description: t('glossaries.toast.updatedDesc', { name: data.name }),
});
} catch (error) {
toast({
variant: 'destructive',
title: 'Error',
description: 'Failed to update glossary. Please try again.',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorUpdate'),
});
throw error;
}
@@ -101,14 +125,14 @@ export default function GlossariesPage() {
setDeleteDialogOpen(false);
setGlossaryToDelete(null);
toast({
title: 'Glossary deleted',
description: 'The glossary has been deleted successfully.',
title: t('glossaries.toast.deleted'),
description: t('glossaries.toast.deletedDesc'),
});
} catch (error) {
toast({
variant: 'destructive',
title: 'Error',
description: 'Failed to delete glossary. Please try again.',
title: t('glossaries.toast.error'),
description: t('glossaries.toast.errorDelete'),
});
}
};
@@ -131,10 +155,8 @@ export default function GlossariesPage() {
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Glossaries</h1>
<p className="text-muted-foreground">
Manage custom terminology for your LLM translations.
</p>
<h1 className="text-2xl font-semibold tracking-tight">{t('glossaries.title')}</h1>
<p className="text-muted-foreground">{t('glossaries.description')}</p>
</div>
<Card>
@@ -144,8 +166,8 @@ export default function GlossariesPage() {
<BookText className="size-4 text-accent" />
</div>
<div>
<CardTitle className="text-base">Your Glossaries</CardTitle>
<CardDescription>Create and manage glossaries for consistent translations</CardDescription>
<CardTitle className="text-base">{t('glossaries.yourGlossaries')}</CardTitle>
<CardDescription>{t('glossaries.yourGlossariesDesc')}</CardDescription>
</div>
</div>
</CardHeader>
@@ -154,11 +176,9 @@ export default function GlossariesPage() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">
{total} glossarie{total !== 1 ? 's' : ''}
</p>
<p className="text-xs text-muted-foreground">
Define term pairs to customize your LLM translations
{t(total !== 1 ? 'glossaries.count_other' : 'glossaries.count_one', { count: String(total) })}
</p>
<p className="text-xs text-muted-foreground">{t('glossaries.defineTerms')}</p>
</div>
<Button
onClick={() => setCreateDialogOpen(true)}
@@ -166,17 +186,15 @@ export default function GlossariesPage() {
className="gap-1.5"
>
<Plus className="size-3.5" />
Create New Glossary
{t('glossaries.createNew')}
</Button>
</div>
{glossaries.length === 0 ? (
<div className="text-center py-8">
<BookText className="size-12 mx-auto text-muted-foreground/50 mb-4" />
<p className="text-muted-foreground">No glossaries yet</p>
<p className="text-sm text-muted-foreground/80">
Create your first glossary to customize translations
</p>
<p className="text-muted-foreground">{t('glossaries.empty')}</p>
<p className="text-sm text-muted-foreground/80">{t('glossaries.emptyDesc')}</p>
</div>
) : (
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@@ -198,15 +216,11 @@ export default function GlossariesPage() {
<Card className="border-border/50">
<CardHeader>
<CardTitle className="text-sm">About Glossaries</CardTitle>
<CardTitle className="text-sm">{t('glossaries.aboutTitle')}</CardTitle>
</CardHeader>
<CardContent className="text-sm text-muted-foreground space-y-2">
<p>
Glossaries let you define custom terminology for your translations. When using LLM translation modes, your terms will be applied to ensure consistent translations.
</p>
<p>
<strong>Format:</strong> Each term has a source (original) and target (translation) pair.
</p>
<p>{t('glossaries.aboutDesc')}</p>
<p><strong>{t('glossaries.aboutFormat')}</strong></p>
</CardContent>
</Card>
@@ -214,7 +228,9 @@ export default function GlossariesPage() {
open={createDialogOpen}
onOpenChange={setCreateDialogOpen}
onCreate={handleCreateGlossary}
onImportTemplate={handleImportTemplate}
isCreating={isCreating}
isImportingTemplate={isImportingTemplate}
/>
{editDialogOpen && (fullGlossary || !isLoadingGlossaryDetail) && (