perf(registratura): lightweight API mode strips base64 attachments from list

ROOT CAUSE: RegistryEntry stores file attachments as base64 strings in JSON.
A single 5MB PDF becomes ~6.7MB of base64. With 6 entries, the exportAll()
endpoint was sending 30-60MB of JSON on every page load  taking 2+ minutes.

Fix: Added ?lightweight=true parameter to /api/storage GET endpoint.
When enabled, stripHeavyFields() recursively removes large 'data' and
'fileData' string fields (>1KB) from JSON values, replacing with '__stripped__'.

Changes:
- /api/storage route.ts: stripHeavyFields() + lightweight query param
- StorageService.export(): accepts { lightweight?: boolean } option
- DatabaseStorageAdapter.export(): passes lightweight flag to API
- LocalStorageAdapter.export(): accepts option (no-op, localStorage is fast)
- useStorage.exportAll(): passes options through
- registry-service.ts: getAllEntries() uses lightweight=true by default
- registry-service.ts: new getFullEntry() loads single entry with full data
- use-registry.ts: exports loadFullEntry() for on-demand full loading
- registratura-module.tsx: handleEdit/handleNavigateEntry load full entry

Result: List loading transfers ~100KB instead of 30-60MB. Editing loads
full data for a single entry on demand (~5-10MB for one entry vs all).
This commit is contained in:
AI Assistant
2026-02-27 22:37:39 +02:00
parent db9bcd7192
commit c22848b471
8 changed files with 128 additions and 39 deletions
+35 -1
View File
@@ -1,10 +1,40 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/core/storage/prisma";
/**
* Strip heavy base64 data from JSON values to enable fast list loading.
* Targets: attachments[].data, versions[].fileData, and any top-level `data`
* field that looks like base64 (>1KB). Replaces with "__stripped__" marker.
*/
function stripHeavyFields(value: unknown): unknown {
if (!value || typeof value !== "object") return value;
if (Array.isArray(value)) return value.map(stripHeavyFields);
const obj = value as Record<string, unknown>;
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(obj)) {
if (k === "data" && typeof v === "string" && v.length > 1024) {
// Strip large base64 data fields, keep a marker
result[k] = "__stripped__";
} else if (k === "fileData" && typeof v === "string" && v.length > 1024) {
result[k] = "__stripped__";
} else if (Array.isArray(v)) {
result[k] = v.map(stripHeavyFields);
} else if (v && typeof v === "object") {
result[k] = stripHeavyFields(v);
} else {
result[k] = v;
}
}
return result;
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams;
const namespace = searchParams.get("namespace");
const key = searchParams.get("key");
const lightweight = searchParams.get("lightweight") === "true";
if (!namespace) {
return NextResponse.json(
@@ -34,7 +64,11 @@ export async function GET(request: NextRequest) {
// Return as a record { [key]: value }
const result: Record<string, any> = {};
for (const item of items) {
result[item.key] = item.value;
if (lightweight) {
result[item.key] = stripHeavyFields(item.value);
} else {
result[item.key] = item.value;
}
}
return NextResponse.json({ items: result });
}