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
+72 -4
View File
@@ -45,6 +45,17 @@ export type DatabaseName = {
* question being asked is only "is this empty, or has something been put in it".
*/
tables: number | null;
/**
* Rows across every table, and the newest timestamp anything carries.
*
* A table count says the schema is there. It says nothing about whether the site has any CONTENT,
* which is the question somebody actually has when looking at two databases with identical
* structure — one of these is the live site and one is a copy, and they look the same until you
* count what is in them.
*/
rows: number | null;
/** The most recent `created_at` anywhere, or null when no table has one. */
lastWrite: string | null;
};
export type DatabasesStatus = {
@@ -111,9 +122,9 @@ export async function readDatabases(): Promise<DatabasesStatus> {
const available = rows.map((entry) => entry.datname);
const describe = async (name: string | null): Promise<DatabaseName> => {
if (!name) return { name: null, exists: null, tables: null };
if (!available.includes(name)) return { name, exists: false, tables: null };
return { name, exists: true, tables: await countTables(name) };
if (!name) return { name: null, exists: null, tables: null, rows: null, lastWrite: null };
if (!available.includes(name)) return { name, exists: false, tables: null, rows: null, lastWrite: null };
return { name, exists: true, ...(await describeContents(name)) };
};
return {
@@ -126,7 +137,64 @@ export async function readDatabases(): Promise<DatabasesStatus> {
}
}
/** Tables in a database's `public` schema. Its own connection, since this asks INSIDE the database. */
/**
* What is actually in a database: tables, rows, and when anything was last written.
*
* One connection for all three, because it asks INSIDE the database and opening three would be three
* connections to answer one question.
*
* Rows are counted with `query_to_xml` over `information_schema`, which is the standard way to get an
* exact count per table without knowing the table names in advance. `reltuples` from the planner would
* be free, and it is an ESTIMATE — it reads as zero on a table nobody has analysed, which is exactly
* the freshly restored database this is meant to reassure somebody about.
*
* `lastWrite` looks only at columns called `created_at`. Most tables here have one and the ones that do
* not are settings rows; a schema-agnostic answer is not available and a wrong one would be worse than
* none, so this returns null rather than guessing when nothing qualifies.
*/
async function describeContents(name: string): Promise<{ tables: number | null; rows: number | null; lastWrite: string | null }> {
const raw = process.env.OFFICERDEV_POSTGRES_URL ?? process.env.POSTGRES_URL;
if (!raw) return { tables: null, rows: null, lastWrite: null };
const url = new URL(raw);
url.pathname = `/${name}`;
const sql = postgres(url.toString(), { max: 1 });
try {
const [counts] = await sql<{ tables: string; rows: string }[]>`
select count(*)::text as tables, coalesce(sum(n), 0)::text as rows from (
select (xpath('/row/c/text()',
query_to_xml(format('select count(*) as c from %I.%I', table_schema, table_name), false, true, '')
))[1]::text::bigint as n
from information_schema.tables
where table_schema = 'public' and table_type = 'BASE TABLE'
) t
`;
const [latest] = await sql<{ at: string | null }[]>`
select max(at)::text as at from (
select (xpath('/row/c/text()',
query_to_xml(format('select max(created_at) as c from %I.%I', table_schema, table_name), false, true, '')
))[1]::text::timestamptz as at
from information_schema.columns
where table_schema = 'public' and column_name = 'created_at'
) t
`;
return {
tables: Number(counts?.tables ?? 0),
rows: Number(counts?.rows ?? 0),
lastWrite: latest?.at ?? null,
};
} catch {
// Exists but will not answer. Null throughout, because "0 rows" about a database that could not be
// read would say the opposite of what is true.
return { tables: null, rows: null, lastWrite: null };
} finally {
await sql.end({ timeout: 5 });
}
}
/** Tables alone, for the places that only need to know whether a schema has been pushed. */
async function countTables(name: string): Promise<number | null> {
const raw = process.env.OFFICERDEV_POSTGRES_URL ?? process.env.POSTGRES_URL;
if (!raw) return null;