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
+36 -20
View File
@@ -1,40 +1,45 @@
import type { StorageService } from '@/core/storage/types';
import type { Tag, TagCategory, TagScope } from './types';
import { v4 as uuid } from 'uuid';
import type { StorageService } from "@/core/storage/types";
import type { Tag, TagCategory, TagScope } from "./types";
import { v4 as uuid } from "uuid";
const NAMESPACE = 'tags';
const NAMESPACE = "tags";
export class TagService {
constructor(private storage: StorageService) {}
async getAllTags(): Promise<Tag[]> {
const keys = await this.storage.list(NAMESPACE);
const all = await this.storage.export(NAMESPACE);
const tags: Tag[] = [];
for (const key of keys) {
const tag = await this.storage.get<Tag>(NAMESPACE, key);
if (tag) tags.push(tag);
for (const value of Object.values(all)) {
if (value) tags.push(value as Tag);
}
return tags;
}
async getTagsByCategory(category: TagCategory): Promise<Tag[]> {
return this.storage.query<Tag>(NAMESPACE, (tag) => tag.category === category);
return this.storage.query<Tag>(
NAMESPACE,
(tag) => tag.category === category,
);
}
async getTagsByScope(scope: TagScope, scopeId?: string): Promise<Tag[]> {
return this.storage.query<Tag>(NAMESPACE, (tag) => {
if (tag.scope !== scope) return false;
if (scope === 'module' && scopeId) return tag.moduleId === scopeId;
if (scope === 'company' && scopeId) return tag.companyId === scopeId;
if (scope === "module" && scopeId) return tag.moduleId === scopeId;
if (scope === "company" && scopeId) return tag.companyId === scopeId;
return true;
});
}
async getChildren(parentId: string): Promise<Tag[]> {
return this.storage.query<Tag>(NAMESPACE, (tag) => tag.parentId === parentId);
return this.storage.query<Tag>(
NAMESPACE,
(tag) => tag.parentId === parentId,
);
}
async createTag(data: Omit<Tag, 'id' | 'createdAt'>): Promise<Tag> {
async createTag(data: Omit<Tag, "id" | "createdAt">): Promise<Tag> {
const tag: Tag = {
...data,
id: uuid(),
@@ -44,10 +49,17 @@ export class TagService {
return tag;
}
async updateTag(id: string, updates: Partial<Omit<Tag, 'id' | 'createdAt'>>): Promise<Tag | null> {
async updateTag(
id: string,
updates: Partial<Omit<Tag, "id" | "createdAt">>,
): Promise<Tag | null> {
const existing = await this.storage.get<Tag>(NAMESPACE, id);
if (!existing) return null;
const updated: Tag = { ...existing, ...updates, updatedAt: new Date().toISOString() };
const updated: Tag = {
...existing,
...updates,
updatedAt: new Date().toISOString(),
};
await this.storage.set(NAMESPACE, id, updated);
return updated;
}
@@ -63,16 +75,20 @@ export class TagService {
async searchTags(query: string): Promise<Tag[]> {
const lower = query.toLowerCase();
return this.storage.query<Tag>(NAMESPACE, (tag) =>
tag.label.toLowerCase().includes(lower) ||
(tag.projectCode?.toLowerCase().includes(lower) ?? false)
return this.storage.query<Tag>(
NAMESPACE,
(tag) =>
tag.label.toLowerCase().includes(lower) ||
(tag.projectCode?.toLowerCase().includes(lower) ?? false),
);
}
/** Bulk import tags (for seed data). Skips tags whose label already exists in same category. */
async importTags(tags: Omit<Tag, 'id' | 'createdAt'>[]): Promise<number> {
async importTags(tags: Omit<Tag, "id" | "createdAt">[]): Promise<number> {
const existing = await this.getAllTags();
const existingKeys = new Set(existing.map((t) => `${t.category}::${t.label}`));
const existingKeys = new Set(
existing.map((t) => `${t.category}::${t.label}`),
);
let imported = 0;
for (const data of tags) {
const key = `${data.category}::${data.label}`;