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

@@ -83,7 +83,7 @@ const statusConfig: Record<string, { label: string; dotClass: string; textClass:
function formatDate(dateString: string): string {
try {
const date = new Date(dateString);
return date.toLocaleDateString("fr-FR", {
return date.toLocaleDateString(undefined, {
day: "2-digit",
month: "short",
year: "numeric",
@@ -208,7 +208,7 @@ export function UserTable({
placeholder="Rechercher par email..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-8 pl-8 text-xs"
className="h-8 ps-8 text-xs"
/>
</div>
</div>
@@ -220,7 +220,7 @@ export function UserTable({
<Table>
<TableHeader>
<TableRow className="hover:bg-transparent">
<TableHead className="h-8 pl-6 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
<TableHead className="h-8 ps-6 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Email
</TableHead>
<TableHead className="h-8 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
@@ -238,7 +238,7 @@ export function UserTable({
<TableHead className="h-8 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Clés
</TableHead>
<TableHead className="h-8 pr-6 text-right text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
<TableHead className="h-8 pe-6 text-end text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
Actions
</TableHead>
</TableRow>
@@ -255,7 +255,7 @@ export function UserTable({
return (
<TableRow key={user.id} className={`group ${hasError ? "bg-destructive/5" : ""}`}>
<TableCell className="pl-6 py-2">
<TableCell className="ps-6 py-2">
<div className="flex flex-col gap-0.5">
<span className="text-xs font-medium text-foreground">
{user.email}
@@ -357,7 +357,7 @@ export function UserTable({
</span>
</TableCell>
<TableCell className="pr-6 py-2 text-right">
<TableCell className="pe-6 py-2 text-end">
<div className="flex justify-end gap-1.5">
<Tooltip>
<TooltipTrigger asChild>

View File

@@ -8,6 +8,7 @@ import { useRevokeApiKey } from "./useRevokeApiKey";
import { UserStats } from "./UserStats";
import { UserTable } from "./UserTable";
import { useToast } from "@/components/ui/toast";
import { useI18n } from "@/lib/i18n";
import type { PlanType } from "./types";
export default function AdminUsersPage() {
@@ -16,19 +17,20 @@ export default function AdminUsersPage() {
const { revokeKey, isRevoking } = useRevokeApiKey();
const { resetPassword, isResetting } = useAdminResetPassword();
const toast = useToast();
const { t } = useI18n();
const handleTierChange = async (userId: string, plan: PlanType) => {
try {
await updateTier({ userId, plan });
toast.success({
title: "Plan mis à jour",
description: `Le plan a été changé vers "${plan}" avec succès.`,
title: t('admin.users.planUpdated'),
description: t('admin.users.planChanged', { plan }),
});
} catch (err) {
const message = err instanceof Error ? err.message : "Erreur inconnue";
const message = err instanceof Error ? err.message : t('admin.users.unknownError');
toast.error({
title: "Erreur",
description: `Impossible de mettre à jour le plan: ${message}`,
title: t('admin.users.error'),
description: t('admin.users.planUpdateError', { message }),
});
throw err;
}
@@ -37,8 +39,8 @@ export default function AdminUsersPage() {
const handleRevokeKeys = async (userId: string, keyIds: string[]) => {
if (!keyIds || keyIds.length === 0) {
toast.warning({
title: "Aucune clé",
description: "Cet utilisateur n'a pas de clés API actives.",
title: t('admin.users.noKeys'),
description: t('admin.users.noKeysDesc'),
});
return;
}
@@ -50,15 +52,15 @@ export default function AdminUsersPage() {
)
);
toast.success({
title: "Clés révoquées",
description: `${keyIds.length} clé${keyIds.length > 1 ? "s" : ""} API ${keyIds.length > 1 ? "ont été révoquées" : "a été révoquée"} avec succès.`,
title: t('admin.users.keysRevoked'),
description: t('admin.users.keysRevokedDesc', { count: keyIds.length }),
});
refetch();
} catch (err) {
const message = err instanceof Error ? err.message : "Erreur inconnue";
const message = err instanceof Error ? err.message : t('admin.users.unknownError');
toast.error({
title: "Erreur",
description: `Impossible de révoquer les clés: ${message}`,
title: t('admin.users.error'),
description: t('admin.users.revokeError', { message }),
});
throw err;
}
@@ -72,8 +74,8 @@ export default function AdminUsersPage() {
<Users className="w-5 h-5 text-blue-400" />
</div>
<div>
<h1 className="text-xl font-semibold text-foreground">Gestion des Utilisateurs</h1>
<p className="text-sm text-muted-foreground">Visualiser et gérer les comptes utilisateurs</p>
<h1 className="text-xl font-semibold text-foreground">{t('admin.users.title')}</h1>
<p className="text-sm text-muted-foreground">{t('admin.users.subtitle')}</p>
</div>
</div>
<div className="bg-destructive/10 border border-destructive/30 rounded-lg p-4">
@@ -82,7 +84,7 @@ export default function AdminUsersPage() {
onClick={() => refetch()}
className="mt-2 text-xs text-destructive hover:underline"
>
Réessayer
{t('admin.users.retry')}
</button>
</div>
</div>
@@ -96,8 +98,8 @@ export default function AdminUsersPage() {
<Users className="w-5 h-5 text-blue-400" />
</div>
<div>
<h1 className="text-xl font-semibold text-foreground">Gestion des Utilisateurs</h1>
<p className="text-sm text-muted-foreground">Visualiser et gérer les comptes utilisateurs</p>
<h1 className="text-xl font-semibold text-foreground">{t('admin.users.title')}</h1>
<p className="text-sm text-muted-foreground">{t('admin.users.subtitle')}</p>
</div>
</div>