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

@@ -15,8 +15,10 @@ import { GenerateKeyDialog } from './GenerateKeyDialog';
import { RevokeKeyDialog } from './RevokeKeyDialog';
import { WebhookSnippet } from './WebhookSnippet';
import { useToast } from '@/components/ui/toast';
import { useI18n } from '@/lib/i18n';
export default function ApiKeysPage() {
const { t } = useI18n();
const { data: user, isLoading: isLoadingUser } = useUser();
const {
keys,
@@ -44,16 +46,15 @@ export default function ApiKeysPage() {
// Handle API errors with specific error codes
useEffect(() => {
if (errorDetails?.code === 'PRO_FEATURE_REQUIRED') {
// Redirect to upgrade prompt will happen via isPro check
setApiError(null);
} else if (errorDetails?.code === 'API_KEY_LIMIT_REACHED') {
setApiError('You have reached the maximum of 10 API keys. Revoke an existing key to generate a new one.');
setApiError(t('apiKeys.limitReachedDesc'));
} else if (errorDetails) {
setApiError(errorDetails.message);
} else {
setApiError(null);
}
}, [errorDetails]);
}, [errorDetails]); // eslint-disable-line react-hooks/exhaustive-deps
const handleRevokeClick = (key: ApiKey) => {
setKeyToRevoke({ id: key.id, name: key.name });
@@ -67,22 +68,22 @@ export default function ApiKeysPage() {
setRevokeDialogOpen(false);
setKeyToRevoke(null);
toast({
title: 'Key revoked',
description: 'The API key has been revoked successfully.',
title: t('apiKeys.keyRevoked'),
description: t('apiKeys.keyRevokedDesc'),
});
} catch (error) {
const revokeError = parseRevokeError();
if (revokeError?.code === 'API_KEY_NOT_FOUND') {
toast({
variant: 'destructive',
title: 'Key Not Found',
description: 'The API key no longer exists. It may have already been revoked.',
title: t('apiKeys.keyNotFound'),
description: t('apiKeys.keyNotFoundDesc'),
});
} else {
toast({
variant: 'destructive',
title: 'Error',
description: revokeError?.message || 'Failed to revoke the API key. Please try again.',
title: t('apiKeys.error'),
description: revokeError?.message || t('apiKeys.revokeError'),
});
}
}
@@ -97,20 +98,20 @@ export default function ApiKeysPage() {
if (genError?.code === 'API_KEY_LIMIT_REACHED') {
toast({
variant: 'destructive',
title: 'Limit Reached',
description: 'You have reached the maximum of 10 API keys. Revoke an existing key to generate a new one.',
title: t('apiKeys.limitReached'),
description: t('apiKeys.limitReachedDesc'),
});
} else if (genError?.code === 'PRO_FEATURE_REQUIRED') {
toast({
variant: 'destructive',
title: 'Pro Feature Required',
description: 'API keys are a Pro feature. Please upgrade your account.',
title: t('apiKeys.proRequired'),
description: t('apiKeys.proRequiredDesc'),
});
} else {
toast({
variant: 'destructive',
title: 'Error',
description: genError?.message || 'Failed to generate API key. Please try again.',
title: t('apiKeys.error'),
description: genError?.message || t('apiKeys.generateError'),
});
}
throw error;
@@ -122,7 +123,7 @@ export default function ApiKeysPage() {
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-muted border-t-foreground mx-auto"></div>
<p className="text-sm text-muted-foreground">Loading...</p>
<p className="text-sm text-muted-foreground">{t('apiKeys.loading')}</p>
</div>
</div>
);
@@ -135,9 +136,9 @@ export default function ApiKeysPage() {
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">API Keys</h1>
<h1 className="text-2xl font-semibold tracking-tight">{t('apiKeys.title')}</h1>
<p className="text-muted-foreground">
Manage your API keys for programmatic access to the translation API.
{t('apiKeys.subtitle')}
</p>
</div>
@@ -155,8 +156,8 @@ export default function ApiKeysPage() {
<Zap className="size-4 text-accent" />
</div>
<div>
<CardTitle className="text-base">API & Automation</CardTitle>
<CardDescription>Generate and manage your API keys for automation workflows</CardDescription>
<CardTitle className="text-base">{t('apiKeys.sectionTitle')}</CardTitle>
<CardDescription>{t('apiKeys.sectionDesc')}</CardDescription>
</div>
</div>
</CardHeader>
@@ -165,13 +166,13 @@ export default function ApiKeysPage() {
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">
{total} of {MAX_API_KEYS} keys used
{t('apiKeys.keysUsed', { total, max: MAX_API_KEYS })}
</p>
<p className="text-xs text-muted-foreground">
{maxKeysReached ? (
<span className="text-amber-600">Maximum keys reached. Revoke a key to generate a new one.</span>
<span className="text-amber-600">{t('apiKeys.maxReached')}</span>
) : (
`You can generate ${MAX_API_KEYS - total} more key${MAX_API_KEYS - total !== 1 ? 's' : ''}.`
t(MAX_API_KEYS - total !== 1 ? 'apiKeys.canGeneratePlural' : 'apiKeys.canGenerate', { count: MAX_API_KEYS - total })
)}
</p>
</div>
@@ -181,7 +182,7 @@ export default function ApiKeysPage() {
className="gap-1.5"
>
<Plus className="size-3.5" />
Generate New Key
{t('apiKeys.generateNew')}
</Button>
</div>