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:
@@ -95,7 +95,7 @@ function TemplateCard({
|
||||
onClick={() => onSelect(template)}
|
||||
disabled={isLoading}
|
||||
className={cn(
|
||||
'flex flex-col gap-2 rounded-lg border p-3 text-left transition-colors disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
'flex flex-col gap-2 rounded-lg border p-3 text-start transition-colors disabled:opacity-50 disabled:cursor-not-allowed',
|
||||
colorClass
|
||||
)}
|
||||
>
|
||||
@@ -477,7 +477,7 @@ export function CreateGlossaryDialog({
|
||||
{t('glossaries.dialog.cancel')}
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!canSubmit}>
|
||||
{isProcessing && <Loader2 className="size-3.5 animate-spin mr-1.5" />}
|
||||
{isProcessing && <Loader2 className="size-3.5 animate-spin me-1.5" />}
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { BookText, Pencil, Trash2 } from 'lucide-react';
|
||||
import type { GlossaryListItem } from './types';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { useI18n, formatDate } from '@/lib/i18n';
|
||||
|
||||
interface GlossaryCardProps {
|
||||
glossary: GlossaryListItem;
|
||||
@@ -30,7 +30,7 @@ export const GlossaryCard = memo(function GlossaryCard({
|
||||
onDelete(glossary.id, glossary.name);
|
||||
}, [glossary.id, glossary.name, onDelete]);
|
||||
|
||||
const formattedDate = new Date(glossary.created_at).toLocaleDateString(locale === 'fr' ? 'fr-FR' : 'en-US', {
|
||||
const formattedDate = formatDate(new Date(glossary.created_at), locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -60,8 +60,7 @@ export function useGlossaries(options: UseGlossariesOptions = {}) {
|
||||
} = useQuery<GlossaryListResponse, ApiClientError>({
|
||||
queryKey: [...GLOSSARIES_QUERY_KEY, page, perPage],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get<GlossaryListResponse>(`/api/v1/glossaries?page=${page}&per_page=${perPage}`);
|
||||
return response.data;
|
||||
return apiClient.get<GlossaryListResponse>(`/api/v1/glossaries?page=${page}&per_page=${perPage}`);
|
||||
},
|
||||
retry: (failureCount, err) => {
|
||||
if (err.status === 403 || err.status === 401) return false;
|
||||
@@ -80,7 +79,7 @@ export function useGlossaries(options: UseGlossariesOptions = {}) {
|
||||
const createMutation = useMutation({
|
||||
mutationFn: async (input: GlossaryCreateInput): Promise<Glossary> => {
|
||||
const response = await apiClient.post<GlossaryDetailResponse>('/api/v1/glossaries', input);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: GLOSSARIES_QUERY_KEY });
|
||||
@@ -90,7 +89,7 @@ export function useGlossaries(options: UseGlossariesOptions = {}) {
|
||||
const updateMutation = useMutation({
|
||||
mutationFn: async ({ id, data }: { id: string; data: GlossaryUpdateInput }): Promise<Glossary> => {
|
||||
const response = await apiClient.patch<GlossaryDetailResponse>(`/api/v1/glossaries/${id}`, data);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: GLOSSARIES_QUERY_KEY });
|
||||
@@ -125,7 +124,7 @@ export function useGlossaries(options: UseGlossariesOptions = {}) {
|
||||
const response = await apiClient.post<GlossaryDetailResponse>(
|
||||
`/api/v1/glossaries/import?${params.toString()}`
|
||||
);
|
||||
return response.data.data;
|
||||
return response.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: GLOSSARIES_QUERY_KEY });
|
||||
@@ -191,8 +190,7 @@ export function useGlossaryTemplates() {
|
||||
const { data, isLoading, error } = useQuery<GlossaryTemplatesResponse, ApiClientError>({
|
||||
queryKey: ['glossary-templates'],
|
||||
queryFn: async () => {
|
||||
const response = await apiClient.get<GlossaryTemplatesResponse>('/api/v1/glossaries/templates/list');
|
||||
return response.data;
|
||||
return apiClient.get<GlossaryTemplatesResponse>('/api/v1/glossaries/templates/list');
|
||||
},
|
||||
staleTime: 5 * 60 * 1000, // templates rarely change
|
||||
retry: 1,
|
||||
@@ -214,8 +212,7 @@ export function useGlossary(id: string | null) {
|
||||
queryKey: [...GLOSSARIES_QUERY_KEY, id],
|
||||
queryFn: async () => {
|
||||
if (!id) throw new Error('Glossary ID is required');
|
||||
const response = await apiClient.get<GlossaryDetailResponse>(`/api/v1/glossaries/${id}`);
|
||||
return response.data;
|
||||
return apiClient.get<GlossaryDetailResponse>(`/api/v1/glossaries/${id}`);
|
||||
},
|
||||
enabled: !!id,
|
||||
retry: (failureCount, err) => {
|
||||
|
||||
Reference in New Issue
Block a user