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

@@ -10,6 +10,7 @@ import type {
AvailableProvider,
} from './types';
import { API_BASE } from '@/lib/config';
import { useTranslationStore } from '@/lib/store';
/** Fallback when API fails — Google is always available server-side */
const FALLBACK_PROVIDERS: AvailableProvider[] = [
@@ -59,8 +60,9 @@ const FALLBACK_LANGUAGES: Language[] = [
];
export function useTranslationConfig(hasFile: boolean): UseTranslationConfigReturn {
const { settings } = useTranslationStore();
const [sourceLang, setSourceLang] = useState('auto');
const [targetLang, setTargetLang] = useState('');
const [targetLang, setTargetLang] = useState(settings.defaultTargetLanguage || '');
const [provider, setProvider] = useState<Provider | null>(null);
const [availableProviders, setAvailableProviders] = useState<AvailableProvider[]>([]);
const [isLoadingProviders, setIsLoadingProviders] = useState(false);
@@ -69,6 +71,13 @@ export function useTranslationConfig(hasFile: boolean): UseTranslationConfigRetu
const [isLoadingLanguages, setIsLoadingLanguages] = useState(false);
const [languagesError, setLanguagesError] = useState<string | null>(null);
// Sync with store default target language
useEffect(() => {
if (settings.defaultTargetLanguage && !targetLang) {
setTargetLang(settings.defaultTargetLanguage);
}
}, [settings.defaultTargetLanguage]); // eslint-disable-line react-hooks/exhaustive-deps
// Fetch available (admin-configured) providers
useEffect(() => {
const controller = new AbortController();
@@ -105,6 +114,16 @@ export function useTranslationConfig(hasFile: boolean): UseTranslationConfigRetu
return () => { controller.abort(); clearTimeout(timeoutId); };
}, []);
// Auto-select first classic provider for non-Pro users
useEffect(() => {
if (isLoadingProviders) return;
if (provider !== null) return;
if (isPro) return;
if (availableProviders.length === 0) return;
const firstClassic = availableProviders.find((p) => p.mode === 'classic');
if (firstClassic) setProvider(firstClassic.id);
}, [availableProviders, isLoadingProviders, isPro, provider]);
// Fetch supported languages
useEffect(() => {
const controller = new AbortController();
@@ -148,11 +167,12 @@ export function useTranslationConfig(hasFile: boolean): UseTranslationConfigRetu
// Check user tier
useEffect(() => {
const checkTier = async () => {
const isProTier = (u: any) => ['pro', 'business', 'enterprise'].includes(u?.plan ?? u?.tier ?? '');
const userStr = localStorage.getItem('user');
if (userStr) {
try {
const user = JSON.parse(userStr);
if (user.tier) { setIsPro(user.tier === 'pro'); return; }
if (user?.plan || user?.tier) { setIsPro(isProTier(user)); return; }
} catch { /* continue */ }
}
try {
@@ -164,7 +184,7 @@ export function useTranslationConfig(hasFile: boolean): UseTranslationConfigRetu
if (response.ok) {
const result = await response.json();
const user = result.data;
setIsPro(user.tier === 'pro');
setIsPro(isProTier(user));
localStorage.setItem('user', JSON.stringify(user));
} else {
setIsPro(false);