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:
@@ -1,40 +1,45 @@
|
|||||||
import type { StorageService } from '@/core/storage/types';
|
import type { StorageService } from "@/core/storage/types";
|
||||||
import type { Tag, TagCategory, TagScope } from './types';
|
import type { Tag, TagCategory, TagScope } from "./types";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
const NAMESPACE = 'tags';
|
const NAMESPACE = "tags";
|
||||||
|
|
||||||
export class TagService {
|
export class TagService {
|
||||||
constructor(private storage: StorageService) {}
|
constructor(private storage: StorageService) {}
|
||||||
|
|
||||||
async getAllTags(): Promise<Tag[]> {
|
async getAllTags(): Promise<Tag[]> {
|
||||||
const keys = await this.storage.list(NAMESPACE);
|
const all = await this.storage.export(NAMESPACE);
|
||||||
const tags: Tag[] = [];
|
const tags: Tag[] = [];
|
||||||
for (const key of keys) {
|
for (const value of Object.values(all)) {
|
||||||
const tag = await this.storage.get<Tag>(NAMESPACE, key);
|
if (value) tags.push(value as Tag);
|
||||||
if (tag) tags.push(tag);
|
|
||||||
}
|
}
|
||||||
return tags;
|
return tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTagsByCategory(category: TagCategory): Promise<Tag[]> {
|
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[]> {
|
async getTagsByScope(scope: TagScope, scopeId?: string): Promise<Tag[]> {
|
||||||
return this.storage.query<Tag>(NAMESPACE, (tag) => {
|
return this.storage.query<Tag>(NAMESPACE, (tag) => {
|
||||||
if (tag.scope !== scope) return false;
|
if (tag.scope !== scope) return false;
|
||||||
if (scope === 'module' && scopeId) return tag.moduleId === scopeId;
|
if (scope === "module" && scopeId) return tag.moduleId === scopeId;
|
||||||
if (scope === 'company' && scopeId) return tag.companyId === scopeId;
|
if (scope === "company" && scopeId) return tag.companyId === scopeId;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getChildren(parentId: string): Promise<Tag[]> {
|
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 = {
|
const tag: Tag = {
|
||||||
...data,
|
...data,
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
@@ -44,10 +49,17 @@ export class TagService {
|
|||||||
return tag;
|
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);
|
const existing = await this.storage.get<Tag>(NAMESPACE, id);
|
||||||
if (!existing) return null;
|
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);
|
await this.storage.set(NAMESPACE, id, updated);
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
@@ -63,16 +75,20 @@ export class TagService {
|
|||||||
|
|
||||||
async searchTags(query: string): Promise<Tag[]> {
|
async searchTags(query: string): Promise<Tag[]> {
|
||||||
const lower = query.toLowerCase();
|
const lower = query.toLowerCase();
|
||||||
return this.storage.query<Tag>(NAMESPACE, (tag) =>
|
return this.storage.query<Tag>(
|
||||||
tag.label.toLowerCase().includes(lower) ||
|
NAMESPACE,
|
||||||
(tag.projectCode?.toLowerCase().includes(lower) ?? false)
|
(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. */
|
/** 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 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;
|
let imported = 0;
|
||||||
for (const data of tags) {
|
for (const data of tags) {
|
||||||
const key = `${data.category}::${data.label}`;
|
const key = `${data.category}::${data.label}`;
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import type { AddressContact, ContactType } from '../types';
|
import type { AddressContact, ContactType } from "../types";
|
||||||
|
|
||||||
const PREFIX = 'contact:';
|
const PREFIX = "contact:";
|
||||||
|
|
||||||
export interface ContactFilters {
|
export interface ContactFilters {
|
||||||
search: string;
|
search: string;
|
||||||
type: ContactType | 'all';
|
type: ContactType | "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useContacts() {
|
export function useContacts() {
|
||||||
const storage = useStorage('address-book');
|
const storage = useStorage("address-book");
|
||||||
const [contacts, setContacts] = useState<AddressContact[]>([]);
|
const [contacts, setContacts] = useState<AddressContact[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filters, setFilters] = useState<ContactFilters>({ search: '', type: 'all' });
|
const [filters, setFilters] = useState<ContactFilters>({
|
||||||
|
search: "",
|
||||||
|
type: "all",
|
||||||
|
});
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const results: AddressContact[] = [];
|
const results: AddressContact[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(PREFIX)) {
|
if (key.startsWith(PREFIX) && value) {
|
||||||
const item = await storage.get<AddressContact>(key);
|
results.push(value as AddressContact);
|
||||||
if (item) results.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => a.name.localeCompare(b.name));
|
results.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
@@ -34,41 +36,60 @@ export function useContacts() {
|
|||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
const addContact = useCallback(async (data: Omit<AddressContact, 'id' | 'createdAt' | 'updatedAt'>) => {
|
const addContact = useCallback(
|
||||||
const now = new Date().toISOString();
|
async (data: Omit<AddressContact, "id" | "createdAt" | "updatedAt">) => {
|
||||||
const contact: AddressContact = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
const now = new Date().toISOString();
|
||||||
await storage.set(`${PREFIX}${contact.id}`, contact);
|
const contact: AddressContact = {
|
||||||
await refresh();
|
...data,
|
||||||
return contact;
|
id: uuid(),
|
||||||
}, [storage, refresh]);
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
await storage.set(`${PREFIX}${contact.id}`, contact);
|
||||||
|
await refresh();
|
||||||
|
return contact;
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const updateContact = useCallback(async (id: string, updates: Partial<AddressContact>) => {
|
const updateContact = useCallback(
|
||||||
const existing = contacts.find((c) => c.id === id);
|
async (id: string, updates: Partial<AddressContact>) => {
|
||||||
if (!existing) return;
|
const existing = contacts.find((c) => c.id === id);
|
||||||
const updated: AddressContact = {
|
if (!existing) return;
|
||||||
...existing,
|
const updated: AddressContact = {
|
||||||
...updates,
|
...existing,
|
||||||
id: existing.id,
|
...updates,
|
||||||
createdAt: existing.createdAt,
|
id: existing.id,
|
||||||
updatedAt: new Date().toISOString(),
|
createdAt: existing.createdAt,
|
||||||
};
|
updatedAt: new Date().toISOString(),
|
||||||
await storage.set(`${PREFIX}${id}`, updated);
|
};
|
||||||
await refresh();
|
await storage.set(`${PREFIX}${id}`, updated);
|
||||||
}, [storage, refresh, contacts]);
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh, contacts],
|
||||||
|
);
|
||||||
|
|
||||||
const removeContact = useCallback(async (id: string) => {
|
const removeContact = useCallback(
|
||||||
await storage.delete(`${PREFIX}${id}`);
|
async (id: string) => {
|
||||||
await refresh();
|
await storage.delete(`${PREFIX}${id}`);
|
||||||
}, [storage, refresh]);
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const updateFilter = useCallback(<K extends keyof ContactFilters>(key: K, value: ContactFilters[K]) => {
|
const updateFilter = useCallback(
|
||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
<K extends keyof ContactFilters>(key: K, value: ContactFilters[K]) => {
|
||||||
}, []);
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredContacts = contacts.filter((c) => {
|
const filteredContacts = contacts.filter((c) => {
|
||||||
if (filters.type !== 'all' && c.type !== filters.type) return false;
|
if (filters.type !== "all" && c.type !== filters.type) return false;
|
||||||
if (filters.search) {
|
if (filters.search) {
|
||||||
const q = filters.search.toLowerCase();
|
const q = filters.search.toLowerCase();
|
||||||
return (
|
return (
|
||||||
@@ -76,12 +97,22 @@ export function useContacts() {
|
|||||||
c.company.toLowerCase().includes(q) ||
|
c.company.toLowerCase().includes(q) ||
|
||||||
c.email.toLowerCase().includes(q) ||
|
c.email.toLowerCase().includes(q) ||
|
||||||
c.phone.includes(q) ||
|
c.phone.includes(q) ||
|
||||||
(c.department ?? '').toLowerCase().includes(q) ||
|
(c.department ?? "").toLowerCase().includes(q) ||
|
||||||
(c.role ?? '').toLowerCase().includes(q)
|
(c.role ?? "").toLowerCase().includes(q)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { contacts: filteredContacts, allContacts: contacts, loading, filters, updateFilter, addContact, updateContact, removeContact, refresh };
|
return {
|
||||||
|
contacts: filteredContacts,
|
||||||
|
allContacts: contacts,
|
||||||
|
loading,
|
||||||
|
filters,
|
||||||
|
updateFilter,
|
||||||
|
addContact,
|
||||||
|
updateContact,
|
||||||
|
removeContact,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import type { ChatMessage, ChatSession } from '../types';
|
import type { ChatMessage, ChatSession } from "../types";
|
||||||
|
|
||||||
const SESSION_PREFIX = 'session:';
|
const SESSION_PREFIX = "session:";
|
||||||
|
|
||||||
export function useChat() {
|
export function useChat() {
|
||||||
const storage = useStorage('ai-chat');
|
const storage = useStorage("ai-chat");
|
||||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||||
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
const [activeSessionId, setActiveSessionId] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -17,12 +17,11 @@ export function useChat() {
|
|||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const results: ChatSession[] = [];
|
const results: ChatSession[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(SESSION_PREFIX)) {
|
if (key.startsWith(SESSION_PREFIX) && value) {
|
||||||
const session = await storage.get<ChatSession>(key);
|
results.push(value as ChatSession);
|
||||||
if (session) results.push(session);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
@@ -31,49 +30,65 @@ export function useChat() {
|
|||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
const createSession = useCallback(async (title?: string) => {
|
const createSession = useCallback(
|
||||||
const session: ChatSession = {
|
async (title?: string) => {
|
||||||
id: uuid(),
|
const session: ChatSession = {
|
||||||
title: title || `Conversație ${new Date().toLocaleDateString('ro-RO')}`,
|
id: uuid(),
|
||||||
messages: [],
|
title: title || `Conversație ${new Date().toLocaleDateString("ro-RO")}`,
|
||||||
createdAt: new Date().toISOString(),
|
messages: [],
|
||||||
};
|
createdAt: new Date().toISOString(),
|
||||||
await storage.set(`${SESSION_PREFIX}${session.id}`, session);
|
};
|
||||||
setSessions((prev) => [session, ...prev]);
|
await storage.set(`${SESSION_PREFIX}${session.id}`, session);
|
||||||
setActiveSessionId(session.id);
|
setSessions((prev) => [session, ...prev]);
|
||||||
return session;
|
setActiveSessionId(session.id);
|
||||||
}, [storage]);
|
return session;
|
||||||
|
},
|
||||||
|
[storage],
|
||||||
|
);
|
||||||
|
|
||||||
const addMessage = useCallback(async (content: string, role: ChatMessage['role']) => {
|
const addMessage = useCallback(
|
||||||
if (!activeSessionId) return;
|
async (content: string, role: ChatMessage["role"]) => {
|
||||||
const message: ChatMessage = {
|
if (!activeSessionId) return;
|
||||||
id: uuid(),
|
const message: ChatMessage = {
|
||||||
role,
|
id: uuid(),
|
||||||
content,
|
role,
|
||||||
timestamp: new Date().toISOString(),
|
content,
|
||||||
};
|
timestamp: new Date().toISOString(),
|
||||||
setSessions((prev) => prev.map((s) => {
|
};
|
||||||
if (s.id !== activeSessionId) return s;
|
setSessions((prev) =>
|
||||||
return { ...s, messages: [...s.messages, message] };
|
prev.map((s) => {
|
||||||
}));
|
if (s.id !== activeSessionId) return s;
|
||||||
// Persist
|
return { ...s, messages: [...s.messages, message] };
|
||||||
const current = sessions.find((s) => s.id === activeSessionId);
|
}),
|
||||||
if (current) {
|
);
|
||||||
const updated = { ...current, messages: [...current.messages, message] };
|
// Persist
|
||||||
await storage.set(`${SESSION_PREFIX}${activeSessionId}`, updated);
|
const current = sessions.find((s) => s.id === activeSessionId);
|
||||||
}
|
if (current) {
|
||||||
return message;
|
const updated = {
|
||||||
}, [storage, activeSessionId, sessions]);
|
...current,
|
||||||
|
messages: [...current.messages, message],
|
||||||
|
};
|
||||||
|
await storage.set(`${SESSION_PREFIX}${activeSessionId}`, updated);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
},
|
||||||
|
[storage, activeSessionId, sessions],
|
||||||
|
);
|
||||||
|
|
||||||
const deleteSession = useCallback(async (id: string) => {
|
const deleteSession = useCallback(
|
||||||
await storage.delete(`${SESSION_PREFIX}${id}`);
|
async (id: string) => {
|
||||||
setSessions((prev) => prev.filter((s) => s.id !== id));
|
await storage.delete(`${SESSION_PREFIX}${id}`);
|
||||||
if (activeSessionId === id) {
|
setSessions((prev) => prev.filter((s) => s.id !== id));
|
||||||
setActiveSessionId(null);
|
if (activeSessionId === id) {
|
||||||
}
|
setActiveSessionId(null);
|
||||||
}, [storage, activeSessionId]);
|
}
|
||||||
|
},
|
||||||
|
[storage, activeSessionId],
|
||||||
|
);
|
||||||
|
|
||||||
const selectSession = useCallback((id: string) => {
|
const selectSession = useCallback((id: string) => {
|
||||||
setActiveSessionId(id);
|
setActiveSessionId(id);
|
||||||
|
|||||||
@@ -1,31 +1,37 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import type { SignatureAsset, SignatureAssetType, AssetVersion } from '../types';
|
import type {
|
||||||
|
SignatureAsset,
|
||||||
|
SignatureAssetType,
|
||||||
|
AssetVersion,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
const PREFIX = 'sig:';
|
const PREFIX = "sig:";
|
||||||
|
|
||||||
export interface SignatureFilters {
|
export interface SignatureFilters {
|
||||||
search: string;
|
search: string;
|
||||||
type: SignatureAssetType | 'all';
|
type: SignatureAssetType | "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSignatures() {
|
export function useSignatures() {
|
||||||
const storage = useStorage('digital-signatures');
|
const storage = useStorage("digital-signatures");
|
||||||
const [assets, setAssets] = useState<SignatureAsset[]>([]);
|
const [assets, setAssets] = useState<SignatureAsset[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filters, setFilters] = useState<SignatureFilters>({ search: '', type: 'all' });
|
const [filters, setFilters] = useState<SignatureFilters>({
|
||||||
|
search: "",
|
||||||
|
type: "all",
|
||||||
|
});
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const results: SignatureAsset[] = [];
|
const results: SignatureAsset[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(PREFIX)) {
|
if (key.startsWith(PREFIX) && value) {
|
||||||
const item = await storage.get<SignatureAsset>(key);
|
results.push(value as SignatureAsset);
|
||||||
if (item) results.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
@@ -34,53 +40,95 @@ export function useSignatures() {
|
|||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
const addAsset = useCallback(async (data: Omit<SignatureAsset, 'id' | 'createdAt' | 'updatedAt'>) => {
|
const addAsset = useCallback(
|
||||||
const now = new Date().toISOString();
|
async (data: Omit<SignatureAsset, "id" | "createdAt" | "updatedAt">) => {
|
||||||
const asset: SignatureAsset = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
const now = new Date().toISOString();
|
||||||
await storage.set(`${PREFIX}${asset.id}`, asset);
|
const asset: SignatureAsset = {
|
||||||
await refresh();
|
...data,
|
||||||
return asset;
|
id: uuid(),
|
||||||
}, [storage, refresh]);
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
await storage.set(`${PREFIX}${asset.id}`, asset);
|
||||||
|
await refresh();
|
||||||
|
return asset;
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const updateAsset = useCallback(async (id: string, updates: Partial<SignatureAsset>) => {
|
const updateAsset = useCallback(
|
||||||
const existing = assets.find((a) => a.id === id);
|
async (id: string, updates: Partial<SignatureAsset>) => {
|
||||||
if (!existing) return;
|
const existing = assets.find((a) => a.id === id);
|
||||||
const updated: SignatureAsset = {
|
if (!existing) return;
|
||||||
...existing, ...updates,
|
const updated: SignatureAsset = {
|
||||||
id: existing.id, createdAt: existing.createdAt,
|
...existing,
|
||||||
updatedAt: new Date().toISOString(),
|
...updates,
|
||||||
};
|
id: existing.id,
|
||||||
await storage.set(`${PREFIX}${id}`, updated);
|
createdAt: existing.createdAt,
|
||||||
await refresh();
|
updatedAt: new Date().toISOString(),
|
||||||
}, [storage, refresh, assets]);
|
};
|
||||||
|
await storage.set(`${PREFIX}${id}`, updated);
|
||||||
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh, assets],
|
||||||
|
);
|
||||||
|
|
||||||
const addVersion = useCallback(async (assetId: string, imageUrl: string, notes: string) => {
|
const addVersion = useCallback(
|
||||||
const existing = assets.find((a) => a.id === assetId);
|
async (assetId: string, imageUrl: string, notes: string) => {
|
||||||
if (!existing) return;
|
const existing = assets.find((a) => a.id === assetId);
|
||||||
const version: AssetVersion = { id: uuid(), imageUrl, notes, createdAt: new Date().toISOString() };
|
if (!existing) return;
|
||||||
const updatedVersions = [...(existing.versions ?? []), version];
|
const version: AssetVersion = {
|
||||||
await updateAsset(assetId, { imageUrl, versions: updatedVersions });
|
id: uuid(),
|
||||||
}, [assets, updateAsset]);
|
imageUrl,
|
||||||
|
notes,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const updatedVersions = [...(existing.versions ?? []), version];
|
||||||
|
await updateAsset(assetId, { imageUrl, versions: updatedVersions });
|
||||||
|
},
|
||||||
|
[assets, updateAsset],
|
||||||
|
);
|
||||||
|
|
||||||
const removeAsset = useCallback(async (id: string) => {
|
const removeAsset = useCallback(
|
||||||
await storage.delete(`${PREFIX}${id}`);
|
async (id: string) => {
|
||||||
await refresh();
|
await storage.delete(`${PREFIX}${id}`);
|
||||||
}, [storage, refresh]);
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const updateFilter = useCallback(<K extends keyof SignatureFilters>(key: K, value: SignatureFilters[K]) => {
|
const updateFilter = useCallback(
|
||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
<K extends keyof SignatureFilters>(key: K, value: SignatureFilters[K]) => {
|
||||||
}, []);
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredAssets = assets.filter((a) => {
|
const filteredAssets = assets.filter((a) => {
|
||||||
if (filters.type !== 'all' && a.type !== filters.type) return false;
|
if (filters.type !== "all" && a.type !== filters.type) return false;
|
||||||
if (filters.search) {
|
if (filters.search) {
|
||||||
const q = filters.search.toLowerCase();
|
const q = filters.search.toLowerCase();
|
||||||
return a.label.toLowerCase().includes(q) || a.owner.toLowerCase().includes(q);
|
return (
|
||||||
|
a.label.toLowerCase().includes(q) || a.owner.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { assets: filteredAssets, allAssets: assets, loading, filters, updateFilter, addAsset, updateAsset, addVersion, removeAsset, refresh };
|
return {
|
||||||
|
assets: filteredAssets,
|
||||||
|
allAssets: assets,
|
||||||
|
loading,
|
||||||
|
filters,
|
||||||
|
updateFilter,
|
||||||
|
addAsset,
|
||||||
|
updateAsset,
|
||||||
|
addVersion,
|
||||||
|
removeAsset,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,22 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import type { SignatureConfig, SavedSignature } from '../types';
|
import type { SignatureConfig, SavedSignature } from "../types";
|
||||||
|
|
||||||
export function useSavedSignatures() {
|
export function useSavedSignatures() {
|
||||||
const storage = useStorage('email-signature');
|
const storage = useStorage("email-signature");
|
||||||
const [saved, setSaved] = useState<SavedSignature[]>([]);
|
const [saved, setSaved] = useState<SavedSignature[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const items: SavedSignature[] = [];
|
const items: SavedSignature[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith('sig:')) {
|
if (key.startsWith("sig:") && value) {
|
||||||
const item = await storage.get<SavedSignature>(key);
|
items.push(value as SavedSignature);
|
||||||
if (item) items.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
items.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
items.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
||||||
@@ -26,26 +25,34 @@ export function useSavedSignatures() {
|
|||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
const save = useCallback(async (label: string, config: SignatureConfig) => {
|
const save = useCallback(
|
||||||
const now = new Date().toISOString();
|
async (label: string, config: SignatureConfig) => {
|
||||||
const entry: SavedSignature = {
|
const now = new Date().toISOString();
|
||||||
id: uuid(),
|
const entry: SavedSignature = {
|
||||||
label,
|
id: uuid(),
|
||||||
config,
|
label,
|
||||||
createdAt: now,
|
config,
|
||||||
updatedAt: now,
|
createdAt: now,
|
||||||
};
|
updatedAt: now,
|
||||||
await storage.set(`sig:${entry.id}`, entry);
|
};
|
||||||
await refresh();
|
await storage.set(`sig:${entry.id}`, entry);
|
||||||
return entry;
|
await refresh();
|
||||||
}, [storage, refresh]);
|
return entry;
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const remove = useCallback(async (id: string) => {
|
const remove = useCallback(
|
||||||
await storage.delete(`sig:${id}`);
|
async (id: string) => {
|
||||||
await refresh();
|
await storage.delete(`sig:${id}`);
|
||||||
}, [storage, refresh]);
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
return { saved, loading, save, remove, refresh };
|
return { saved, loading, save, remove, refresh };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,11 @@ export function useReservations() {
|
|||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const results: DeskReservation[] = [];
|
const results: DeskReservation[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(PREFIX)) {
|
if (key.startsWith(PREFIX) && value) {
|
||||||
const item = await storage.get<DeskReservation>(key);
|
results.push(value as DeskReservation);
|
||||||
if (item) results.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => a.date.localeCompare(b.date));
|
results.sort((a, b) => a.date.localeCompare(b.date));
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ export function ServerRack({ items }: ServerRackProps) {
|
|||||||
return result;
|
return result;
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
|
||||||
|
// Reverse so U1 is at the bottom (standard rack numbering)
|
||||||
|
const displaySlots = useMemo(() => [...slots].reverse(), [slots]);
|
||||||
|
|
||||||
const rackItems = items.filter((i) => i.rackPosition);
|
const rackItems = items.filter((i) => i.rackPosition);
|
||||||
|
|
||||||
if (rackItems.length === 0) {
|
if (rackItems.length === 0) {
|
||||||
@@ -97,7 +100,7 @@ export function ServerRack({ items }: ServerRackProps) {
|
|||||||
|
|
||||||
{/* Units */}
|
{/* Units */}
|
||||||
<div className="p-1 space-y-px">
|
<div className="p-1 space-y-px">
|
||||||
{slots.map((slot) => {
|
{displaySlots.map((slot) => {
|
||||||
if (!slot.isStart && slot.item) return null;
|
if (!slot.isStart && slot.item) return null;
|
||||||
|
|
||||||
const heightPx = slot.height * 24;
|
const heightPx = slot.height * 24;
|
||||||
|
|||||||
@@ -27,12 +27,11 @@ export function useInventory() {
|
|||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const results: InventoryItem[] = [];
|
const results: InventoryItem[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(PREFIX)) {
|
if (key.startsWith(PREFIX) && value) {
|
||||||
const item = await storage.get<InventoryItem>(key);
|
results.push(value as InventoryItem);
|
||||||
if (item) results.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
results.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
|
|||||||
@@ -1,31 +1,33 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import type { VaultEntry, VaultEntryCategory } from '../types';
|
import type { VaultEntry, VaultEntryCategory } from "../types";
|
||||||
|
|
||||||
const PREFIX = 'vault:';
|
const PREFIX = "vault:";
|
||||||
|
|
||||||
export interface VaultFilters {
|
export interface VaultFilters {
|
||||||
search: string;
|
search: string;
|
||||||
category: VaultEntryCategory | 'all';
|
category: VaultEntryCategory | "all";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useVault() {
|
export function useVault() {
|
||||||
const storage = useStorage('password-vault');
|
const storage = useStorage("password-vault");
|
||||||
const [entries, setEntries] = useState<VaultEntry[]>([]);
|
const [entries, setEntries] = useState<VaultEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [filters, setFilters] = useState<VaultFilters>({ search: '', category: 'all' });
|
const [filters, setFilters] = useState<VaultFilters>({
|
||||||
|
search: "",
|
||||||
|
category: "all",
|
||||||
|
});
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const results: VaultEntry[] = [];
|
const results: VaultEntry[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(PREFIX)) {
|
if (key.startsWith(PREFIX) && value) {
|
||||||
const item = await storage.get<VaultEntry>(key);
|
results.push(value as VaultEntry);
|
||||||
if (item) results.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => a.label.localeCompare(b.label));
|
results.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
@@ -34,41 +36,81 @@ export function useVault() {
|
|||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => {
|
||||||
|
refresh();
|
||||||
|
}, [refresh]);
|
||||||
|
|
||||||
const addEntry = useCallback(async (data: Omit<VaultEntry, 'id' | 'createdAt' | 'updatedAt'>) => {
|
const addEntry = useCallback(
|
||||||
const now = new Date().toISOString();
|
async (data: Omit<VaultEntry, "id" | "createdAt" | "updatedAt">) => {
|
||||||
const entry: VaultEntry = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
const now = new Date().toISOString();
|
||||||
await storage.set(`${PREFIX}${entry.id}`, entry);
|
const entry: VaultEntry = {
|
||||||
await refresh();
|
...data,
|
||||||
return entry;
|
id: uuid(),
|
||||||
}, [storage, refresh]);
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
await storage.set(`${PREFIX}${entry.id}`, entry);
|
||||||
|
await refresh();
|
||||||
|
return entry;
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const updateEntry = useCallback(async (id: string, updates: Partial<VaultEntry>) => {
|
const updateEntry = useCallback(
|
||||||
const existing = entries.find((e) => e.id === id);
|
async (id: string, updates: Partial<VaultEntry>) => {
|
||||||
if (!existing) return;
|
const existing = entries.find((e) => e.id === id);
|
||||||
const updated = { ...existing, ...updates, id: existing.id, createdAt: existing.createdAt, updatedAt: new Date().toISOString() };
|
if (!existing) return;
|
||||||
await storage.set(`${PREFIX}${id}`, updated);
|
const updated = {
|
||||||
await refresh();
|
...existing,
|
||||||
}, [storage, refresh, entries]);
|
...updates,
|
||||||
|
id: existing.id,
|
||||||
|
createdAt: existing.createdAt,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
await storage.set(`${PREFIX}${id}`, updated);
|
||||||
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh, entries],
|
||||||
|
);
|
||||||
|
|
||||||
const removeEntry = useCallback(async (id: string) => {
|
const removeEntry = useCallback(
|
||||||
await storage.delete(`${PREFIX}${id}`);
|
async (id: string) => {
|
||||||
await refresh();
|
await storage.delete(`${PREFIX}${id}`);
|
||||||
}, [storage, refresh]);
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const updateFilter = useCallback(<K extends keyof VaultFilters>(key: K, value: VaultFilters[K]) => {
|
const updateFilter = useCallback(
|
||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
<K extends keyof VaultFilters>(key: K, value: VaultFilters[K]) => {
|
||||||
}, []);
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredEntries = entries.filter((e) => {
|
const filteredEntries = entries.filter((e) => {
|
||||||
if (filters.category !== 'all' && e.category !== filters.category) return false;
|
if (filters.category !== "all" && e.category !== filters.category)
|
||||||
|
return false;
|
||||||
if (filters.search) {
|
if (filters.search) {
|
||||||
const q = filters.search.toLowerCase();
|
const q = filters.search.toLowerCase();
|
||||||
return e.label.toLowerCase().includes(q) || e.username.toLowerCase().includes(q) || e.url.toLowerCase().includes(q);
|
return (
|
||||||
|
e.label.toLowerCase().includes(q) ||
|
||||||
|
e.username.toLowerCase().includes(q) ||
|
||||||
|
e.url.toLowerCase().includes(q)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { entries: filteredEntries, allEntries: entries, loading, filters, updateFilter, addEntry, updateEntry, removeEntry, refresh };
|
return {
|
||||||
|
entries: filteredEntries,
|
||||||
|
allEntries: entries,
|
||||||
|
loading,
|
||||||
|
filters,
|
||||||
|
updateFilter,
|
||||||
|
addEntry,
|
||||||
|
updateEntry,
|
||||||
|
removeEntry,
|
||||||
|
refresh,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,42 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useMemo, useEffect } from 'react';
|
import { useState, useCallback, useMemo, useEffect } from "react";
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import type { PromptTemplate, PromptHistoryEntry, OutputMode } from '../types';
|
import type { PromptTemplate, PromptHistoryEntry, OutputMode } from "../types";
|
||||||
import { BUILTIN_TEMPLATES } from '../services/builtin-templates';
|
import { BUILTIN_TEMPLATES } from "../services/builtin-templates";
|
||||||
import { composePrompt } from '../services/prompt-composer';
|
import { composePrompt } from "../services/prompt-composer";
|
||||||
|
|
||||||
const HISTORY_PREFIX = 'history:';
|
const HISTORY_PREFIX = "history:";
|
||||||
const TEMPLATE_PREFIX = 'template:';
|
const TEMPLATE_PREFIX = "template:";
|
||||||
|
|
||||||
export function usePromptGenerator() {
|
export function usePromptGenerator() {
|
||||||
const storage = useStorage('prompt-generator');
|
const storage = useStorage("prompt-generator");
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<PromptTemplate | null>(null);
|
const [selectedTemplate, setSelectedTemplate] =
|
||||||
|
useState<PromptTemplate | null>(null);
|
||||||
const [values, setValues] = useState<Record<string, unknown>>({});
|
const [values, setValues] = useState<Record<string, unknown>>({});
|
||||||
const [customTemplates, setCustomTemplates] = useState<PromptTemplate[]>([]);
|
const [customTemplates, setCustomTemplates] = useState<PromptTemplate[]>([]);
|
||||||
const [history, setHistory] = useState<PromptHistoryEntry[]>([]);
|
const [history, setHistory] = useState<PromptHistoryEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const allTemplates = useMemo(() => [...BUILTIN_TEMPLATES, ...customTemplates], [customTemplates]);
|
const allTemplates = useMemo(
|
||||||
|
() => [...BUILTIN_TEMPLATES, ...customTemplates],
|
||||||
|
[customTemplates],
|
||||||
|
);
|
||||||
|
|
||||||
// Load custom templates and history
|
// Load custom templates and history
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const templates: PromptTemplate[] = [];
|
const templates: PromptTemplate[] = [];
|
||||||
const entries: PromptHistoryEntry[] = [];
|
const entries: PromptHistoryEntry[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
|
if (!value) continue;
|
||||||
if (key.startsWith(TEMPLATE_PREFIX)) {
|
if (key.startsWith(TEMPLATE_PREFIX)) {
|
||||||
const t = await storage.get<PromptTemplate>(key);
|
templates.push(value as PromptTemplate);
|
||||||
if (t) templates.push(t);
|
|
||||||
} else if (key.startsWith(HISTORY_PREFIX)) {
|
} else if (key.startsWith(HISTORY_PREFIX)) {
|
||||||
const h = await storage.get<PromptHistoryEntry>(key);
|
entries.push(value as PromptHistoryEntry);
|
||||||
if (h) entries.push(h);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
@@ -59,7 +62,7 @@ export function usePromptGenerator() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const composedPrompt = useMemo(() => {
|
const composedPrompt = useMemo(() => {
|
||||||
if (!selectedTemplate) return '';
|
if (!selectedTemplate) return "";
|
||||||
return composePrompt(selectedTemplate, values);
|
return composePrompt(selectedTemplate, values);
|
||||||
}, [selectedTemplate, values]);
|
}, [selectedTemplate, values]);
|
||||||
|
|
||||||
@@ -74,7 +77,10 @@ export function usePromptGenerator() {
|
|||||||
composedPrompt,
|
composedPrompt,
|
||||||
outputMode: selectedTemplate.outputMode,
|
outputMode: selectedTemplate.outputMode,
|
||||||
providerProfile: selectedTemplate.providerProfile ?? null,
|
providerProfile: selectedTemplate.providerProfile ?? null,
|
||||||
safetyBlocks: selectedTemplate.safetyBlocks?.filter((s) => s.enabled).map((s) => s.id) ?? [],
|
safetyBlocks:
|
||||||
|
selectedTemplate.safetyBlocks
|
||||||
|
?.filter((s) => s.enabled)
|
||||||
|
.map((s) => s.id) ?? [],
|
||||||
tags: selectedTemplate.tags,
|
tags: selectedTemplate.tags,
|
||||||
isFavorite: false,
|
isFavorite: false,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
@@ -84,10 +90,13 @@ export function usePromptGenerator() {
|
|||||||
return entry;
|
return entry;
|
||||||
}, [storage, selectedTemplate, values, composedPrompt]);
|
}, [storage, selectedTemplate, values, composedPrompt]);
|
||||||
|
|
||||||
const deleteHistoryEntry = useCallback(async (id: string) => {
|
const deleteHistoryEntry = useCallback(
|
||||||
await storage.delete(`${HISTORY_PREFIX}${id}`);
|
async (id: string) => {
|
||||||
setHistory((prev) => prev.filter((h) => h.id !== id));
|
await storage.delete(`${HISTORY_PREFIX}${id}`);
|
||||||
}, [storage]);
|
setHistory((prev) => prev.filter((h) => h.id !== id));
|
||||||
|
},
|
||||||
|
[storage],
|
||||||
|
);
|
||||||
|
|
||||||
const clearSelection = useCallback(() => {
|
const clearSelection = useCallback(() => {
|
||||||
setSelectedTemplate(null);
|
setSelectedTemplate(null);
|
||||||
|
|||||||
@@ -59,7 +59,8 @@ export function useRegistry() {
|
|||||||
async (
|
async (
|
||||||
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
data: Omit<RegistryEntry, "id" | "number" | "createdAt" | "updatedAt">,
|
||||||
) => {
|
) => {
|
||||||
// Fetch fresh entries to prevent duplicate numbers from stale state
|
// Fetch fresh entries to prevent duplicate numbers from stale state.
|
||||||
|
// This single call replaces what was previously list() + N×get().
|
||||||
const freshEntries = await getAllEntries(storage);
|
const freshEntries = await getAllEntries(storage);
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
const number = generateRegistryNumber(
|
const number = generateRegistryNumber(
|
||||||
@@ -76,10 +77,11 @@ export function useRegistry() {
|
|||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
};
|
};
|
||||||
await saveEntry(storage, entry);
|
await saveEntry(storage, entry);
|
||||||
await refresh();
|
// Update local state directly to avoid a second full fetch
|
||||||
|
setEntries((prev) => [entry, ...prev]);
|
||||||
return entry;
|
return entry;
|
||||||
},
|
},
|
||||||
[storage, refresh],
|
[storage],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateEntry = useCallback(
|
const updateEntry = useCallback(
|
||||||
@@ -108,29 +110,36 @@ export function useRegistry() {
|
|||||||
[storage, refresh],
|
[storage, refresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
/** Close an entry and optionally its linked entries */
|
/** Close an entry and optionally its linked entries.
|
||||||
|
* Batches all saves, then does a single refresh at the end.
|
||||||
|
*/
|
||||||
const closeEntry = useCallback(
|
const closeEntry = useCallback(
|
||||||
async (id: string, closeLinked: boolean) => {
|
async (id: string, closeLinked: boolean) => {
|
||||||
const entry = entries.find((e) => e.id === id);
|
const entry = entries.find((e) => e.id === id);
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
await updateEntry(id, { status: "inchis" });
|
// Save main entry as closed
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const closedMain: RegistryEntry = {
|
||||||
|
...entry,
|
||||||
|
status: "inchis",
|
||||||
|
updatedAt: now,
|
||||||
|
};
|
||||||
|
await saveEntry(storage, closedMain);
|
||||||
|
// Close linked entries in parallel
|
||||||
const linked = entry.linkedEntryIds ?? [];
|
const linked = entry.linkedEntryIds ?? [];
|
||||||
if (closeLinked && linked.length > 0) {
|
if (closeLinked && linked.length > 0) {
|
||||||
for (const linkedId of linked) {
|
const saves = linked
|
||||||
const linked = entries.find((e) => e.id === linkedId);
|
.map((linkedId) => entries.find((e) => e.id === linkedId))
|
||||||
if (linked && linked.status !== "inchis") {
|
.filter((e): e is RegistryEntry => !!e && e.status !== "inchis")
|
||||||
const updatedLinked: RegistryEntry = {
|
.map((e) =>
|
||||||
...linked,
|
saveEntry(storage, { ...e, status: "inchis", updatedAt: now }),
|
||||||
status: "inchis",
|
);
|
||||||
updatedAt: new Date().toISOString(),
|
await Promise.all(saves);
|
||||||
};
|
|
||||||
await saveEntry(storage, updatedLinked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await refresh();
|
|
||||||
}
|
}
|
||||||
|
// Single refresh at the end
|
||||||
|
await refresh();
|
||||||
},
|
},
|
||||||
[entries, updateEntry, storage, refresh],
|
[entries, storage, refresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateFilter = useCallback(
|
const updateFilter = useCallback(
|
||||||
|
|||||||
@@ -8,17 +8,22 @@ export interface RegistryStorage {
|
|||||||
set<T>(key: string, value: T): Promise<void>;
|
set<T>(key: string, value: T): Promise<void>;
|
||||||
delete(key: string): Promise<void>;
|
delete(key: string): Promise<void>;
|
||||||
list(): Promise<string[]>;
|
list(): Promise<string[]>;
|
||||||
|
exportAll(): Promise<Record<string, unknown>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all registry entries in a SINGLE request.
|
||||||
|
* Uses exportAll() which fetches namespace data in one HTTP call,
|
||||||
|
* avoiding the N+1 pattern (list keys → get each one individually).
|
||||||
|
*/
|
||||||
export async function getAllEntries(
|
export async function getAllEntries(
|
||||||
storage: RegistryStorage,
|
storage: RegistryStorage,
|
||||||
): Promise<RegistryEntry[]> {
|
): Promise<RegistryEntry[]> {
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const entries: RegistryEntry[] = [];
|
const entries: RegistryEntry[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(STORAGE_PREFIX)) {
|
if (key.startsWith(STORAGE_PREFIX) && value) {
|
||||||
const entry = await storage.get<RegistryEntry>(key);
|
entries.push(value as RegistryEntry);
|
||||||
if (entry) entries.push(entry);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
entries.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||||
|
|||||||
@@ -1,32 +1,35 @@
|
|||||||
'use client';
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useStorage } from '@/core/storage';
|
import { useStorage } from "@/core/storage";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
import type { WordTemplate, TemplateCategory } from '../types';
|
import type { WordTemplate, TemplateCategory } from "../types";
|
||||||
|
|
||||||
const PREFIX = 'tpl:';
|
const PREFIX = "tpl:";
|
||||||
|
|
||||||
export interface TemplateFilters {
|
export interface TemplateFilters {
|
||||||
search: string;
|
search: string;
|
||||||
category: TemplateCategory | 'all';
|
category: TemplateCategory | "all";
|
||||||
company: string;
|
company: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useTemplates() {
|
export function useTemplates() {
|
||||||
const storage = useStorage('word-templates');
|
const storage = useStorage("word-templates");
|
||||||
const [templates, setTemplates] = useState<WordTemplate[]>([]);
|
const [templates, setTemplates] = useState<WordTemplate[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
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 () => {
|
const refresh = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const keys = await storage.list();
|
const all = await storage.exportAll();
|
||||||
const results: WordTemplate[] = [];
|
const results: WordTemplate[] = [];
|
||||||
for (const key of keys) {
|
for (const [key, value] of Object.entries(all)) {
|
||||||
if (key.startsWith(PREFIX)) {
|
if (key.startsWith(PREFIX) && value) {
|
||||||
const item = await storage.get<WordTemplate>(key);
|
results.push(value as WordTemplate);
|
||||||
if (item) results.push(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
results.sort((a, b) => a.name.localeCompare(b.name));
|
results.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
@@ -35,63 +38,103 @@ export function useTemplates() {
|
|||||||
}, [storage]);
|
}, [storage]);
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
// 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 addTemplate = useCallback(
|
||||||
const now = new Date().toISOString();
|
async (data: Omit<WordTemplate, "id" | "createdAt" | "updatedAt">) => {
|
||||||
const template: WordTemplate = { ...data, id: uuid(), createdAt: now, updatedAt: now };
|
const now = new Date().toISOString();
|
||||||
await storage.set(`${PREFIX}${template.id}`, template);
|
const template: WordTemplate = {
|
||||||
await refresh();
|
...data,
|
||||||
return template;
|
id: uuid(),
|
||||||
}, [storage, refresh]);
|
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 updateTemplate = useCallback(
|
||||||
const existing = templates.find((t) => t.id === id);
|
async (id: string, updates: Partial<WordTemplate>) => {
|
||||||
if (!existing) return;
|
const existing = templates.find((t) => t.id === id);
|
||||||
const updated: WordTemplate = {
|
if (!existing) return;
|
||||||
...existing, ...updates,
|
const updated: WordTemplate = {
|
||||||
id: existing.id, createdAt: existing.createdAt,
|
...existing,
|
||||||
updatedAt: new Date().toISOString(),
|
...updates,
|
||||||
};
|
id: existing.id,
|
||||||
await storage.set(`${PREFIX}${id}`, updated);
|
createdAt: existing.createdAt,
|
||||||
await refresh();
|
updatedAt: new Date().toISOString(),
|
||||||
}, [storage, refresh, templates]);
|
};
|
||||||
|
await storage.set(`${PREFIX}${id}`, updated);
|
||||||
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh, templates],
|
||||||
|
);
|
||||||
|
|
||||||
const cloneTemplate = useCallback(async (id: string) => {
|
const cloneTemplate = useCallback(
|
||||||
const existing = templates.find((t) => t.id === id);
|
async (id: string) => {
|
||||||
if (!existing) return;
|
const existing = templates.find((t) => t.id === id);
|
||||||
const now = new Date().toISOString();
|
if (!existing) return;
|
||||||
const cloned: WordTemplate = {
|
const now = new Date().toISOString();
|
||||||
...existing,
|
const cloned: WordTemplate = {
|
||||||
id: uuid(),
|
...existing,
|
||||||
name: `${existing.name} (copie)`,
|
id: uuid(),
|
||||||
clonedFrom: existing.id,
|
name: `${existing.name} (copie)`,
|
||||||
createdAt: now,
|
clonedFrom: existing.id,
|
||||||
updatedAt: now,
|
createdAt: now,
|
||||||
};
|
updatedAt: now,
|
||||||
await storage.set(`${PREFIX}${cloned.id}`, cloned);
|
};
|
||||||
await refresh();
|
await storage.set(`${PREFIX}${cloned.id}`, cloned);
|
||||||
return cloned;
|
await refresh();
|
||||||
}, [storage, refresh, templates]);
|
return cloned;
|
||||||
|
},
|
||||||
|
[storage, refresh, templates],
|
||||||
|
);
|
||||||
|
|
||||||
const removeTemplate = useCallback(async (id: string) => {
|
const removeTemplate = useCallback(
|
||||||
await storage.delete(`${PREFIX}${id}`);
|
async (id: string) => {
|
||||||
await refresh();
|
await storage.delete(`${PREFIX}${id}`);
|
||||||
}, [storage, refresh]);
|
await refresh();
|
||||||
|
},
|
||||||
|
[storage, refresh],
|
||||||
|
);
|
||||||
|
|
||||||
const updateFilter = useCallback(<K extends keyof TemplateFilters>(key: K, value: TemplateFilters[K]) => {
|
const updateFilter = useCallback(
|
||||||
setFilters((prev) => ({ ...prev, [key]: value }));
|
<K extends keyof TemplateFilters>(key: K, value: TemplateFilters[K]) => {
|
||||||
}, []);
|
setFilters((prev) => ({ ...prev, [key]: value }));
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const filteredTemplates = templates.filter((t) => {
|
const filteredTemplates = templates.filter((t) => {
|
||||||
if (filters.category !== 'all' && t.category !== filters.category) return false;
|
if (filters.category !== "all" && t.category !== filters.category)
|
||||||
if (filters.company !== 'all' && t.company !== filters.company) return false;
|
return false;
|
||||||
|
if (filters.company !== "all" && t.company !== filters.company)
|
||||||
|
return false;
|
||||||
if (filters.search) {
|
if (filters.search) {
|
||||||
const q = filters.search.toLowerCase();
|
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 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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user