From c78420e0be631b7fb01161874e0f324ba119bf81 Mon Sep 17 00:00:00 2001 From: Marco Sadjadi Date: Wed, 20 May 2026 17:23:24 +0200 Subject: [PATCH] =?UTF-8?q?fix(wizard):=20fork=20409=20=E2=80=94=20auto-un?= =?UTF-8?q?ique=20slug=20+=20editable=20name/slug=20in=20fork=20step?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: forking a template POSTed /v1/servers with slug = trySlug(template.title), a fixed value. If the user had any server with that slug already (e.g. forking the same template twice, or a name collision), the create returned 409 slug_taken — and the fork wizard skips Step 1, so there was no slug field to fix it with. The user was stuck (8 repeated 409s in the report). Fix: - On fork setup, after the fork call, GET /v1/servers and auto-unique the default slug: echo-demo-template -> echo-demo-template-2 -> -3 ... against the user's existing slugs. Lookup failure is non-fatal (slug field is editable). - Fork step 2 now renders editable Name + Slug fields in the fork banner ('must be unique in your workspace' hint) — the normal wizard has these in Step 1, which the fork flow skips, so they belong here. - slug_taken build error now reads 'The slug "x" is already used by one of your servers — change the Slug field above' instead of the raw code. Note: the SES lockdown-install.js and content.js 'query' errors in the report are browser extensions, not the app. Verified: forked echo-demo-template (whose base slug was already taken) — slug auto-filled echo-demo-template-2, build succeeded, container live on :4111, template fork_count incremented to 2. --- apps/web/app/(dashboard)/servers/new/page.tsx | 77 +++++++++++++++---- 1 file changed, 63 insertions(+), 14 deletions(-) diff --git a/apps/web/app/(dashboard)/servers/new/page.tsx b/apps/web/app/(dashboard)/servers/new/page.tsx index c7fbf18..3479ed1 100644 --- a/apps/web/app/(dashboard)/servers/new/page.tsx +++ b/apps/web/app/(dashboard)/servers/new/page.tsx @@ -130,8 +130,27 @@ export default function NewServerPage() { }; }>(`/v1/templates/${templateSlug}/fork`, { method: 'POST', body: '{}' }); if (cancelled) return; + + // Auto-unique the slug against the user's existing servers so the + // default fork doesn't 409 (e.g. forking the same template twice). + let uniqueSlug = trySlug(res.template.title); + try { + const own = await apiFetch<{ servers: { slug: string }[] }>('/v1/servers'); + const taken = new Set(own.servers.map((s) => s.slug)); + if (taken.has(uniqueSlug)) { + const base = uniqueSlug; + let n = 2; + while (taken.has(`${base}-${n}`)) n++; + uniqueSlug = `${base}-${n}`; + } + } catch { + // If the lookup fails we still proceed; the slug field is editable + // and the build surfaces a clear slug_taken error. + } + if (cancelled) return; + setName(res.template.title); - setSlug(trySlug(res.template.title)); + setSlug(uniqueSlug); setPrompt(`Fork of "${res.template.title}" template.`); setForkedTemplateId(res.templateId); setForkedTemplateTitle(res.template.title); @@ -325,7 +344,12 @@ export default function NewServerPage() { setStep('building'); } catch (e) { const detail = (e as { detail?: { error?: string; detail?: unknown } }).detail; - setError(detail?.error ?? (e as Error).message); + const code = detail?.error; + setError( + code === 'slug_taken' + ? `The slug "${slug}" is already used by one of your servers — change the Slug field above.` + : (code ?? (e as Error).message), + ); } } @@ -424,19 +448,44 @@ export default function NewServerPage() { {step === 'confirm' && preview && editable && (
{forkedTemplateTitle && ( -
-
- Forking {forkedTemplateTitle} — fill in - your own credentials below. The template author never sees them. +
+
+
+ Forking {forkedTemplateTitle} — name + your copy and fill in your own credentials. The template author never sees them. +
+ + Template ↗ + +
+
+
+ + { + setName(e.target.value); + if (!slug || slug === trySlug(name)) setSlug(trySlug(e.target.value)); + }} + /> +
+
+ + setSlug(trySlug(e.target.value))} + /> +
- - Template ↗ -
)}