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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -23,7 +23,7 @@
})();
</script>
<script type="module" crossorigin src="/assets/index-BnMOLFk0.js"></script>
<script type="module" crossorigin src="/assets/index-Cto7ZSSt.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-pvHlBxfG.css">
</head>
<body>

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) {