fix: Harden instance rename against interleaving and lost unwinds

This commit is contained in:
2026-08-12 11:08:36 +00:00
parent f534b74066
commit cdc50b7aaf
10 changed files with 220 additions and 64 deletions
+40 -20
View File
@@ -14,14 +14,22 @@ import { baseSlug, hostFor, slugError } from "@/lib/slug";
*
* The host preview is drawn from lib/slug.ts, a mirror of the Go rules. It can
* disagree with the server; the 409 that comes back is the answer that counts.
*
* movesHost is what separates a rename that moves a DNS host from one that only
* changes a label. Self-hosted instances and unprovisioned cloud placeholders
* have no address, so every word about old links breaking and signing in again
* is false for them — and a preview host they will never live at is worse than
* no preview at all.
*/
export function RenamePanel({
currentName,
currentSlug,
movesHost,
onRename,
}: {
currentName: string;
currentSlug: string;
movesHost: boolean;
onRename: (name: string) => Promise<RenameResult>;
}) {
const [open, setOpen] = useState(false);
@@ -45,6 +53,10 @@ export function RenamePanel({
const res = await onRename(name);
setDone(res);
setOpen(false);
// The input is prefilled with the current name, and the current name
// is now this one. Leaving the old text in would make the next open
// look like an edit already in progress.
setValue(res.name);
} catch (err) {
setError(err instanceof ApiError ? err.message : "Rename failed. Try again.");
} finally {
@@ -52,26 +64,34 @@ export function RenamePanel({
}
}
if (done) {
const host = done.login_url || `https://${hostFor(done.slug)}`;
return (
<Note tone="warn">
<span className="grid gap-2">
<span>
This instance is now <strong>{done.name}</strong>, at{" "}
<span className="font-mono">{hostFor(done.slug)}</span>. The old address has stopped working, and your
sign-in does not follow it you will need to sign in again there.
</span>
<a href={host} className="justify-self-start font-mono text-[0.78rem] text-accent underline">
Open {hostFor(done.slug)} &rarr;
</a>
</span>
</Note>
);
}
// The note sits ABOVE the control rather than replacing it. A rename is not
// a one-shot action — a customer who mistypes the new name needs the panel
// back, and returning early here left them with a success message and no way
// to correct it short of a reload.
return (
<div className="grid gap-3">
{done &&
(movesHost ? (
<Note tone="warn">
<span className="grid gap-2">
<span>
This instance is now <strong>{done.name}</strong>, at{" "}
<span className="font-mono">{hostFor(done.slug)}</span>. The old address has stopped working, and
your sign-in does not follow it you will need to sign in again there.
</span>
<a
href={done.login_url || `https://${hostFor(done.slug)}`}
className="justify-self-start font-mono text-[0.78rem] text-accent underline"
>
Open {hostFor(done.slug)} &rarr;
</a>
</span>
</Note>
) : (
<Note tone="warn">
This instance is now <strong>{done.name}</strong>.
</Note>
))}
{open && (
<Field
label="Instance name"
@@ -79,7 +99,7 @@ export function RenamePanel({
onChange={(e) => setValue(e.target.value)}
error={error ?? (name ? invalid : undefined)}
hint={
name && !invalid ? (
movesHost && name && !invalid ? (
<>
Moves to <span className="font-mono">{hostFor(derived)}</span>
{derived === currentSlug && " — the address does not change"}
@@ -99,7 +119,7 @@ export function RenamePanel({
>
{busy ? "Renaming…" : "Rename instance"}
</Button>
{open && (
{open && movesHost && (
<span className="text-[0.82rem] text-ink-3">
Anyone signed in will need to sign in again at the new address, and links to the old one stop working.
</span>