From f448111e77450456e4507f5a2addf525b596cee5 Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Tue, 24 Mar 2026 17:29:02 +0300 Subject: [PATCH] fix: block renaming main project variant; dynamic page titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ErrCannotRenameMainVariant; ProjectService.Update now returns this error if the caller tries to change the Variant of a main project (empty Variant) — ensures there is always exactly one main - Handle ErrCannotRenameMainVariant in PUT /api/projects/:uuid with 400 - Set document.title dynamically from breadcrumb data: - Configurator: "CODE / variant / Config name — QuoteForge" - Project detail: "CODE / variant — QuoteForge" Co-Authored-By: Claude Sonnet 4.6 --- internal/services/project.go | 8 +++++++- web/templates/index.html | 1 + web/templates/project_detail.html | 1 + 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/services/project.go b/internal/services/project.go index 3d67188..d5f3421 100644 --- a/internal/services/project.go +++ b/internal/services/project.go @@ -21,6 +21,7 @@ var ( ErrProjectCodeExists = errors.New("project code and variant already exist") ErrCannotDeleteMainVariant = errors.New("cannot delete main variant") ErrReservedMainVariant = errors.New("variant name 'main' is reserved") + ErrCannotRenameMainVariant = errors.New("cannot rename main variant") ) type ProjectService struct { @@ -108,7 +109,12 @@ func (s *ProjectService) Update(projectUUID, ownerUsername string, req *UpdatePr localProject.Code = code } if req.Variant != nil { - localProject.Variant = strings.TrimSpace(*req.Variant) + newVariant := strings.TrimSpace(*req.Variant) + // Block renaming of the main variant (empty Variant) — there must always be a main. + if strings.TrimSpace(localProject.Variant) == "" && newVariant != "" { + return nil, ErrCannotRenameMainVariant + } + localProject.Variant = newVariant if err := validateProjectVariantName(localProject.Variant); err != nil { return nil, err } diff --git a/web/templates/index.html b/web/templates/index.html index 7a9c025..0434dc6 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -477,6 +477,7 @@ function updateConfigBreadcrumbs() { configEl.textContent = truncateBreadcrumbSpecName(fullConfigName); configEl.title = fullConfigName; versionEl.textContent = 'main'; + document.title = code + ' / ' + variant + ' / ' + fullConfigName + ' — QuoteForge'; const configNameLinkEl = document.getElementById('breadcrumb-config-name-link'); if (configNameLinkEl && configUUID) { configNameLinkEl.href = '/configs/' + configUUID + '/revisions'; diff --git a/web/templates/project_detail.html b/web/templates/project_detail.html index b59b457..b060214 100644 --- a/web/templates/project_detail.html +++ b/web/templates/project_detail.html @@ -363,6 +363,7 @@ function renderVariantSelect() { if (item.uuid === projectUUID) { option.className += ' font-semibold text-gray-900'; label.textContent = variantLabel; + document.title = (project && project.code ? project.code : '—') + ' / ' + variantLabel + ' — QuoteForge'; } option.textContent = variantLabel; option.onclick = function() {