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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user