perf: logbook memoization and pagination, debounced realtime, lazy exceljs

- Precompute per-entry spend/caffeine/sugar map; paginate ledger at 50 rows
- Debounce realtime refetches (500ms trailing) and suppress during bulk import
- Show import progress (Importing X of N)
- Dynamic-import exceljs: entry chunk 1640 -> 700 kB
- Skeleton placeholders for initial load on Overview/Trends/Logbook
This commit is contained in:
nh9961
2026-07-19 21:00:51 +00:00
parent bb5750e928
commit 9e8f3c55d6
12 changed files with 247 additions and 61 deletions
+40 -8
View File
@@ -51,7 +51,7 @@ import {
parseUserLimits,
} from "./lib/userLimits";
import { createExcelExport, downloadBlob, parseExcelImport } from "./lib/excel";
import { groupByDay, groupByFlavour, groupByWeek, humanDateTime, makeId } from "./lib/metrics";
import { buildEntryDerived, groupByDay, groupByFlavour, groupByWeek, humanDateTime, makeId } from "./lib/metrics";
import { exportPayload, parseImport } from "./lib/storage";
import { useToasts } from "./lib/toasts";
import type {
@@ -60,6 +60,7 @@ import type {
EntryDraft,
Filters,
ImportPreview,
ImportProgress,
RedBullEntry,
SetupStatus,
UserLimits,
@@ -101,6 +102,7 @@ function App() {
const [busyAction, setBusyAction] = useState<string | null>(null);
const [syncError, setSyncError] = useState("");
const [importPreview, setImportPreview] = useState<ImportPreview | null>(null);
const [importProgress, setImportProgress] = useState<ImportProgress | null>(null);
const [userLimits, setUserLimits] = useState<UserLimits>({});
const [limitConfirmOpen, setLimitConfirmOpen] = useState(false);
const [limitConfirmMessage, setLimitConfirmMessage] = useState("");
@@ -110,6 +112,8 @@ function App() {
const { pushToast } = useToasts();
const excelFileInputRef = useRef<HTMLInputElement>(null);
const jsonFileInputRef = useRef<HTMLInputElement>(null);
const importInFlightRef = useRef(false);
const refreshDebounceRef = useRef<number | null>(null);
useEffect(() => {
localStorage.setItem(THEME_STORAGE_KEY, normaliseThemeId(themeId));
@@ -203,16 +207,33 @@ function App() {
useEffect(() => {
if (!user) return undefined;
// Trailing debounce: collapse bursts of row events (e.g. bulk imports) into one refetch.
const scheduleRefresh = () => {
if (refreshDebounceRef.current !== null) {
window.clearTimeout(refreshDebounceRef.current);
}
refreshDebounceRef.current = window.setTimeout(() => {
refreshDebounceRef.current = null;
void refreshEntries(user.$id, false);
}, 500);
};
const unsubscribe = client.subscribe<Record<string, unknown>>(
Channel.tablesdb(appwriteConfig.databaseId).table(appwriteConfig.collectionId).row(),
(event) => {
if (event.payload?.userId === user.$id) {
void refreshEntries(user.$id, false);
}
// Bulk imports update local state directly; their row events need no refetch.
if (importInFlightRef.current) return;
if (event.payload?.userId === user.$id) scheduleRefresh();
},
);
return () => unsubscribe();
return () => {
if (refreshDebounceRef.current !== null) {
window.clearTimeout(refreshDebounceRef.current);
refreshDebounceRef.current = null;
}
unsubscribe();
};
}, [refreshEntries, user]);
useEffect(() => {
@@ -227,6 +248,7 @@ function App() {
() => mergedFlavours(entries.map((entry) => entry.flavour)),
[entries],
);
const entryDerived = useMemo(() => buildEntryDerived(entries), [entries]);
const entriesInView = useMemo(
() => sortEntries(applyFilters(entries, filters)),
[entries, filters],
@@ -538,14 +560,18 @@ function App() {
setBusyAction("confirm-excel-import");
setSyncError("");
importInFlightRef.current = true;
setImportProgress({ done: 0, total: drafts.length });
try {
const saved = await createEntries(user.$id, drafts);
const saved = await createEntries(user.$id, drafts, (done, total) => setImportProgress({ done, total }));
setEntries((current) => sortEntries([...saved, ...current]));
setImportPreview(null);
setNotice(`${saved.length} Excel row${saved.length === 1 ? "" : "s"} saved to Appwrite.`);
} catch (error) {
setSyncError(appwriteErrorMessage(error));
} finally {
importInFlightRef.current = false;
setImportProgress(null);
setBusyAction(null);
}
}
@@ -567,12 +593,13 @@ function App() {
setNotice("No new JSON entries found.");
return;
}
importInFlightRef.current = true;
const saved = await createEntries(user.$id, uniqueDrafts.map((draft) => ({ ...draft, source: "json" })));
setEntries((current) => sortEntries([...saved, ...current]));
setNotice(`${saved.length} JSON entr${saved.length === 1 ? "y" : "ies"} saved to Appwrite.`);
setEntries((current) => sortEntries([...saved, ...current])); setNotice(`${saved.length} JSON entr${saved.length === 1 ? "y" : "ies"} saved to Appwrite.`);
} catch (error) {
setSyncError(error instanceof Error ? error.message : "JSON import failed.");
} finally {
importInFlightRef.current = false;
if (jsonFileInputRef.current) jsonFileInputRef.current.value = "";
setBusyAction(null);
}
@@ -675,6 +702,7 @@ function App() {
user={user}
userLimits={userLimits}
limitCheck={limitCheck}
loading={dataLoading}
onQuickAdd={(item) => void quickAdd(item)}
onAdd={openNewEntry}
onScan={openBarcodeScanner}
@@ -687,8 +715,10 @@ function App() {
<LogbookView
entries={entriesInView}
totalEntries={entries.length}
entryDerived={entryDerived}
filters={filters}
flavours={allFlavours}
loading={dataLoading}
deletingId={deletingId}
onFilterChange={setFilters}
onAdd={openNewEntry}
@@ -711,6 +741,7 @@ function App() {
filters={filters}
flavours={allFlavours}
userLimits={userLimits}
loading={dataLoading}
onFilterChange={setFilters}
onSaveLimits={(next) => void saveUserLimits(next)}
/>
@@ -781,6 +812,7 @@ function App() {
<ImportPreviewModal
busy={busyAction === "confirm-excel-import"}
preview={importPreview}
importProgress={importProgress}
onClose={() => setImportPreview(null)}
onConfirm={() => void confirmExcelImport()}
/>