added cache for dashboard and storage pages

This commit is contained in:
onysd 2026-09-08 02:29:28 +03:00
parent eb69c8500c
commit 5c16371ae0
7 changed files with 81 additions and 15 deletions

View file

@ -25,6 +25,7 @@ import {
} from "lucide-react";
import { useEffect, useState, type ReactNode } from "react";
import { api, errorMessage } from "../api";
import { clearAdminCache } from "../lib/cache";
import { permissionBotVerificationReview, permissionServerManage, permissionAdminsManage,
permissionAccountsRead,
permissionChannelsRead,
@ -242,6 +243,11 @@ export function Shell({
const thirdPartyVerificationHidden = useThirdPartyVerificationHidden();
async function logout() {
await api.logout().catch(() => undefined);
// Drop the cached figures with the session. Without this the next operator
// to sign in on this tab would open the dashboard on the previous one's
// numbers -- briefly, but from an account that may not be allowed to see
// them at all.
clearAdminCache();
onLogout();
}

View file

@ -0,0 +1,35 @@
// Last-known values for screens that are visited repeatedly.
//
// Navigating back to the dashboard used to re-mount it with empty state, so an
// operator watched the same numbers redraw from skeletons every time. The
// screens already refresh themselves; what they lacked was something to show
// while that happens. Reading from here on mount means the page opens on the
// figures it had, and the refresh quietly replaces them.
//
// Kept in module memory rather than localStorage on purpose. This is admin data
// -- account counts, storage figures -- and it has no business outliving the
// tab or sitting on disk. It dies on reload, and clearAdminCache() drops it at
// sign-out so the next operator in the same tab never sees the previous one's
// figures.
const store = new Map<string, unknown>();
export function cacheGet<T>(key: string): T | undefined {
return store.get(key) as T | undefined;
}
export function cacheSet<T>(key: string, value: T): void {
store.set(key, value);
}
export function clearAdminCache(): void {
store.clear();
}
// Keys live here rather than as loose strings at each call site, so a typo
// cannot quietly create a second cache that never hits.
export const cacheKeys = {
dashboard: "dashboard",
storageStats: "storage.stats",
storageAccounts: "storage.accounts"
} as const;

View file

@ -17,13 +17,19 @@ import {
} from "lucide-react";
import { type ReactNode, useEffect, useState } from "react";
import { api } from "../api";
import { cacheGet, cacheKeys, cacheSet } from "../lib/cache";
import { Alert } from "../components/ui";
import type { Navigate } from "../routing";
import { formatBytes, formatQuantity } from "../lib/format";
import type { DashboardResponse } from "../types";
export function Dashboard({ navigate }: { navigate: Navigate }) {
const [data, setData] = useState<DashboardResponse | null>(null);
// Seeded from the last values this session saw, so coming back to the
// dashboard opens on the numbers instead of on a grid of skeletons. The
// 15s refresh below still runs and replaces them.
const [data, setData] = useState<DashboardResponse | null>(
() => cacheGet<DashboardResponse>(cacheKeys.dashboard) ?? null
);
const [error, setError] = useState("");
useEffect(() => {
@ -31,6 +37,7 @@ export function Dashboard({ navigate }: { navigate: Navigate }) {
async function load() {
try {
const res = await api.dashboard();
cacheSet(cacheKeys.dashboard, res);
if (!cancelled) setData(res);
} catch (err) {
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load dashboard");

View file

@ -2,6 +2,7 @@ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronDown, ChevronRight, Loader2, Re
import { useEffect, useMemo, useState } from "react";
import { createPortal } from "react-dom";
import { api, errorMessage } from "../api";
import { cacheGet, cacheKeys, cacheSet } from "../lib/cache";
import { ActionButton } from "../components/ActionButton";
import { Alert, EmptyRow, LoadingRow, Metric, PageFrame, QueryPanel, SectionHead } from "../components/ui";
import { displayUsername, formatBytes, formatQuantity } from "../lib/format";
@ -39,12 +40,18 @@ function SortableHeader({
}
function StorageOverviewTab({ navigate }: { navigate: Navigate }) {
const [stats, setStats] = useState<StorageStatsResponse | null>(null);
// Seeded from this session's last figures, so returning to Storage opens on
// numbers rather than on shimmering placeholders. loadStats still runs.
const [stats, setStats] = useState<StorageStatsResponse | null>(
() => cacheGet<StorageStatsResponse>(cacheKeys.storageStats) ?? null
);
// Tracked separately from `error`: loadStats deliberately swallows its
// failure so it can't block the account list, which would otherwise leave
// the metric skeletons shimmering forever on a stats-only outage.
const [statsFailed, setStatsFailed] = useState(false);
const [rows, setRows] = useState<AccountStorageRow[]>([]);
const [rows, setRows] = useState<AccountStorageRow[]>(
() => cacheGet<AccountStorageRow[]>(cacheKeys.storageAccounts) ?? []
);
const [hasMore, setHasMore] = useState(false);
const [offset, setOffset] = useState(0);
const [busy, setBusy] = useState(false);
@ -55,7 +62,9 @@ function StorageOverviewTab({ navigate }: { navigate: Navigate }) {
async function loadStats() {
try {
setStats(await api.storageStats());
const next = await api.storageStats();
cacheSet(cacheKeys.storageStats, next);
setStats(next);
setStatsFailed(false);
} catch {
// Stats are a header nicety; a failure here shouldn't block the list.
@ -77,7 +86,16 @@ function StorageOverviewTab({ navigate }: { navigate: Navigate }) {
try {
const result = await api.storageAccounts(params);
const page = result.rows ?? [];
setRows((current) => (next ? [...current, ...page] : page));
setRows((current) => {
const merged = next ? [...current, ...page] : page;
// Only the first page is worth keeping: it is what the screen opens on,
// and caching an appended list would restore a scroll position nobody
// asked for.
if (!next) {
cacheSet(cacheKeys.storageAccounts, merged);
}
return merged;
});
setOffset(result.next_offset);
setHasMore(Boolean(result.has_more));
} catch (err) {