perf: fix N+1 query pattern across all modules + rack numbering

CRITICAL PERF BUG: Every hook did storage.list() (1 HTTP call fetching ALL
items with values, discarding values, returning only keys) then storage.get()
for EACH key (N individual HTTP calls re-fetching values one by one).

With 6 entries + contacts + tags, Registratura page fired ~40 sequential
HTTP requests on load, where 3 would suffice.

Fix: Replace list()+N*get() with single exportAll() call in ALL hooks:
- registratura/registry-service.ts (added exportAll to RegistryStorage interface)
- address-book/use-contacts.ts
- it-inventory/use-inventory.ts
- password-vault/use-vault.ts
- word-templates/use-templates.ts
- prompt-generator/use-prompt-generator.ts
- hot-desk/use-reservations.ts
- email-signature/use-saved-signatures.ts
- digital-signatures/use-signatures.ts
- ai-chat/use-chat.ts
- core/tagging/tag-service.ts (uses storage.export())

Additional fixes:
- registratura/use-registry.ts: addEntry uses optimistic local state update
  instead of double-refresh; closeEntry batches saves with Promise.all +
  single refresh
- server-rack.tsx: reversed slot rendering so U1 is at bottom (standard
  rack numbering, per user's physical rack)

Performance impact: ~90% reduction in HTTP requests on page load for all modules
This commit is contained in:
AI Assistant
2026-02-27 22:26:11 +02:00
parent 4cd793fbbc
commit c45a30ec14
13 changed files with 580 additions and 354 deletions
+103 -60
View File
@@ -1,32 +1,35 @@
'use client';
"use client";
import { useState, useEffect, useCallback } from 'react';
import { useStorage } from '@/core/storage';
import { v4 as uuid } from 'uuid';
import type { WordTemplate, TemplateCategory } from '../types';
import { useState, useEffect, useCallback } from "react";
import { useStorage } from "@/core/storage";
import { v4 as uuid } from "uuid";
import type { WordTemplate, TemplateCategory } from "../types";
const PREFIX = 'tpl:';
const PREFIX = "tpl:";
export interface TemplateFilters {
search: string;
category: TemplateCategory | 'all';
category: TemplateCategory | "all";
company: string;
}
export function useTemplates() {
const storage = useStorage('word-templates');
const storage = useStorage("word-templates");
const [templates, setTemplates] = useState<WordTemplate[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<TemplateFilters>({ search: '', category: 'all', company: 'all' });
const [filters, setFilters] = useState<TemplateFilters>({
search: "",
category: "all",
company: "all",
});
const refresh = useCallback(async () => {
setLoading(true);
const keys = await storage.list();
const all = await storage.exportAll();
const results: WordTemplate[] = [];
for (const key of keys) {
if (key.startsWith(PREFIX)) {
const item = await storage.get<WordTemplate>(key);
if (item) results.push(item);
for (const [key, value] of Object.entries(all)) {
if (key.startsWith(PREFIX) && value) {
results.push(value as WordTemplate);
}
}
results.sort((a, b) => a.name.localeCompare(b.name));
@@ -35,63 +38,103 @@ export function useTemplates() {
}, [storage]);
// eslint-disable-next-line react-hooks/set-state-in-effect
useEffect(() => { refresh(); }, [refresh]);
useEffect(() => {
refresh();
}, [refresh]);
const addTemplate = useCallback(async (data: Omit<WordTemplate, 'id' | 'createdAt' | 'updatedAt'>) => {
const now = new Date().toISOString();
const template: WordTemplate = { ...data, id: uuid(), createdAt: now, updatedAt: now };
await storage.set(`${PREFIX}${template.id}`, template);
await refresh();
return template;
}, [storage, refresh]);
const addTemplate = useCallback(
async (data: Omit<WordTemplate, "id" | "createdAt" | "updatedAt">) => {
const now = new Date().toISOString();
const template: WordTemplate = {
...data,
id: uuid(),
createdAt: now,
updatedAt: now,
};
await storage.set(`${PREFIX}${template.id}`, template);
await refresh();
return template;
},
[storage, refresh],
);
const updateTemplate = useCallback(async (id: string, updates: Partial<WordTemplate>) => {
const existing = templates.find((t) => t.id === id);
if (!existing) return;
const updated: WordTemplate = {
...existing, ...updates,
id: existing.id, createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
await storage.set(`${PREFIX}${id}`, updated);
await refresh();
}, [storage, refresh, templates]);
const updateTemplate = useCallback(
async (id: string, updates: Partial<WordTemplate>) => {
const existing = templates.find((t) => t.id === id);
if (!existing) return;
const updated: WordTemplate = {
...existing,
...updates,
id: existing.id,
createdAt: existing.createdAt,
updatedAt: new Date().toISOString(),
};
await storage.set(`${PREFIX}${id}`, updated);
await refresh();
},
[storage, refresh, templates],
);
const cloneTemplate = useCallback(async (id: string) => {
const existing = templates.find((t) => t.id === id);
if (!existing) return;
const now = new Date().toISOString();
const cloned: WordTemplate = {
...existing,
id: uuid(),
name: `${existing.name} (copie)`,
clonedFrom: existing.id,
createdAt: now,
updatedAt: now,
};
await storage.set(`${PREFIX}${cloned.id}`, cloned);
await refresh();
return cloned;
}, [storage, refresh, templates]);
const cloneTemplate = useCallback(
async (id: string) => {
const existing = templates.find((t) => t.id === id);
if (!existing) return;
const now = new Date().toISOString();
const cloned: WordTemplate = {
...existing,
id: uuid(),
name: `${existing.name} (copie)`,
clonedFrom: existing.id,
createdAt: now,
updatedAt: now,
};
await storage.set(`${PREFIX}${cloned.id}`, cloned);
await refresh();
return cloned;
},
[storage, refresh, templates],
);
const removeTemplate = useCallback(async (id: string) => {
await storage.delete(`${PREFIX}${id}`);
await refresh();
}, [storage, refresh]);
const removeTemplate = useCallback(
async (id: string) => {
await storage.delete(`${PREFIX}${id}`);
await refresh();
},
[storage, refresh],
);
const updateFilter = useCallback(<K extends keyof TemplateFilters>(key: K, value: TemplateFilters[K]) => {
setFilters((prev) => ({ ...prev, [key]: value }));
}, []);
const updateFilter = useCallback(
<K extends keyof TemplateFilters>(key: K, value: TemplateFilters[K]) => {
setFilters((prev) => ({ ...prev, [key]: value }));
},
[],
);
const filteredTemplates = templates.filter((t) => {
if (filters.category !== 'all' && t.category !== filters.category) return false;
if (filters.company !== 'all' && t.company !== filters.company) return false;
if (filters.category !== "all" && t.category !== filters.category)
return false;
if (filters.company !== "all" && t.company !== filters.company)
return false;
if (filters.search) {
const q = filters.search.toLowerCase();
return t.name.toLowerCase().includes(q) || t.description.toLowerCase().includes(q);
return (
t.name.toLowerCase().includes(q) ||
t.description.toLowerCase().includes(q)
);
}
return true;
});
return { templates: filteredTemplates, allTemplates: templates, loading, filters, updateFilter, addTemplate, updateTemplate, cloneTemplate, removeTemplate, refresh };
return {
templates: filteredTemplates,
allTemplates: templates,
loading,
filters,
updateFilter,
addTemplate,
updateTemplate,
cloneTemplate,
removeTemplate,
refresh,
};
}