5 Commits
Author SHA1 Message Date
pastilhasandClaude Opus 5 af31b57cdf 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>
2026-08-29 13:32:42 +01:00
pastilhasandClaude Opus 5 f7df5f8b10 the plugin tells the site what its databases are called
Both names, on every push, whichever target it is. The site's guard checks that
the two environments expect DIFFERENT databases, so it needs both to do that at
all — and passing only the one being pushed to would leave the other falling
back to the name the repository shipped with, which is a comparison against
something nobody configured.

This makes the plugin the authority on what the site's databases are called,
which is the whole point of the change on the other side: the site stopped
hardcoding them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:42:24 +01:00
pastilhasandClaude Opus 5 0971764fca once both databases are there, the form becomes two rows of actions
A text box invites an edit nobody is here to make. Named and created, the two
things somebody wants are per-database actions, so the inputs and the button go
and each database becomes a label, a table count, and what can be done to it.

Push schema runs the WEBSITE's own `db:push`. Not drizzle-kit driven from here
with a config this plugin assembles — the schema, the drizzle version and the
push command all belong to that repository, and reimplementing any of them would
be a second definition of the site's database going stale on its own schedule.
This supplies the connection and runs their script.

The connection goes in the environment, which works because src/env-file.ts
consults .env files first and process.env last. So an ambient value is used only
where no file in the checkout sets that key, and this cannot quietly override a
connection somebody wrote down.

Their name guard still applies and should: env.ts refuses to connect unless the
database name matches what FOFOS_ENV expects. A plugin that worked around that
would be defeating the one check standing between development and live data.

"Schema pushed" is a COUNT of tables in public, not a comparison against the
site's expected tables — that list would be a second copy of the site's schema,
stale the first time it gains a table, and the question being asked is only
whether anything is in there yet.

Restore is present and disabled, gated on the schema being pushed. It is not
wired: which backup it reads from is not decided, and guessing at the source of
a restore is not a thing to do with a client's data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:14:39 +01:00
pastilhasandClaude Opus 5 30c52e6449 one button, and it appears when there is something to press
Two buttons was the wrong model, and it failed in the worst way: Create keyed
off the SAVED names, so on a fresh screen with both fields filled it did not
render at all — and the hint explaining why was behind the same condition. Two
filled fields, one Save, and nothing saying what came next.

Naming and creating are not separate decisions from the outside. You type two
names because you want two databases. So one button that records them and
creates whatever is missing, labelled with whichever of those it is about to do,
and disabled only when there is genuinely nothing to press.

The tick beside each field now asks pg_database about the text in the BOX rather
than the text last saved. That is the question somebody typing a name has, and
answering it about a different name was worse than silence.

The PUT that only stored names goes with the second button. Nothing called it,
and a second way to write the same two columns is a second thing that can drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:09:54 +01:00
pastilhasandClaude Opus 5 a1fe543dff two database names, and a button that creates what is missing
Production and development, stored as NAMES rather than URLs. The site runs on
the same Postgres Officer is already connected to — that is the whole reason a
name is enough — and a URL here would be a second copy of a host, a port and a
credential that already exist in the platform's configuration, free to drift
from it and free to point at another machine.

Naming and creating are separate, and were worth two buttons. A database that
already exists still needs its name recorded, and making Create the only way to
save would leave nobody able to point the site at something already set up. The
tick beside each field comes from pg_database, not from what was typed — the
useful question is whether the thing exists, and that came apart from "somebody
saved a name" the moment the two actions split.

CREATE DATABASE cannot be parameterised, so the name goes into the statement as
text and the identifier check is the entire security boundary. It is therefore
strict rather than accommodating — lowercase, leading letter or underscore,
nothing else — and it is re-run immediately before the statement is built,
because the gap between storing a name and using it is exactly where a check
that happened earlier stops being a check. Postgres folds unquoted identifiers
to lowercase, so accepting `Fofos` would create `fofos` and then report a name
nobody could find; refusing is kinder than silently changing it.

Officer's own database and the Postgres templates are refused outright. Pointing
the site at Officer's database would not read as a configuration mistake — it
would read as the platform corrupting itself.

Nothing here drops a database, and nothing should: `fofos_website` and
`fofos_old` are one keystroke apart and one of them holds the client's site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 12:06:40 +01:00