the databases say what is in them, and the block folds away when it is done

Two databases with identical structure look identical until you count what is in
them — one is the live site and one is a copy. So a row now reads "18 tables ·
155 rows" with the time anything was last written, rather than only reporting
that a schema exists.

Counted with query_to_xml over information_schema, which is exact. reltuples
from the planner would be free and is an ESTIMATE: it reads as zero on a table
nobody has analysed, which is precisely the freshly restored database this is
meant to reassure somebody about. That is not a hypothetical — it is why
pg_stat_user_tables reported an empty database earlier today that had 155 rows
in it.

And the block collapses with a check once everything is in order, on the same
terms as Root location: both databases existing with a schema, and backups
configured. Not backups having RUN — a site set up an hour ago has taken none,
and calling that incomplete is telling somebody to fix what is not broken.

Seeded from /summary so the first paint is already right, then kept live by what
the block and BackupsRow know. The `null` sentinel in `collapsed` is left alone
while unsettled, which is the bug that stopped Root location folding at all.

BackupsRow reports its bit from an EFFECT, not during render: calling the
parent's setter mid-render is an update to a different component, which React
refuses outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-29 13:32:42 +01:00
co-authored by Claude Opus 5
parent fb234ede5d
commit af31b57cdf
4 changed files with 190 additions and 9 deletions
+21 -1
View File
@@ -67,7 +67,15 @@ const size = (bytes: number) =>
const when = (iso: string | null) => (iso ? new Date(iso).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }) : null);
export const BackupsRow = ({ prodName, devName }: { prodName: string; devName: string }) => {
export const BackupsRow = ({
prodName,
devName,
onSettled,
}: {
prodName: string;
devName: string;
onSettled?: (settled: boolean) => void;
}) => {
const client = useClient();
const clientRef = useRef(client);
clientRef.current = client;
@@ -110,6 +118,18 @@ export const BackupsRow = ({ prodName, devName }: { prodName: string; devName: s
void load();
}, [load]);
/*
One bit upward, so the block above can decide whether the whole section is finished.
In an effect and not during render. Calling the parent's setter while this component renders is an
update to a DIFFERENT component mid-render, which React refuses outright — and the version of this
that did it would have thrown on the first paint.
*/
const settledUp = Boolean(settings?.cron && settings?.sshHost && settings?.sshPath);
useEffect(() => {
onSettled?.(settledUp);
}, [settledUp, onSettled]);
const save = async () => {
setBusy('save');
setError(null);