From 8508ee2921a7b017964ac4fd565ab68aeea56916 Mon Sep 17 00:00:00 2001 From: Michael Chus Date: Mon, 16 Feb 2026 21:25:22 +0300 Subject: [PATCH] Fix sync errors for duplicate projects and add modal scrolling Root cause: Projects with duplicate (code, variant) pairs fail to sync due to unique constraint on server. Example: multiple "OPS-1934" projects with variant="Dell" where one already exists on server. Fixes: 1. Sync service now detects duplicate (code, variant) on server and links local project to existing server project instead of failing 2. Local repair checks for duplicate (code, variant) pairs and deduplicates by appending UUID suffix to variant 3. Modal now scrollable with fixed header/footer (max-h-90vh) This allows users to sync projects that were created offline with conflicting codes/variants without losing data. Co-Authored-By: Claude Opus 4.6 --- internal/localdb/localdb.go | 18 +++++++++++++++++- internal/services/sync/service.go | 23 +++++++++++++++++++++-- web/templates/base.html | 16 ++++++++++------ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/internal/localdb/localdb.go b/internal/localdb/localdb.go index 11b2c87..110312d 100644 --- a/internal/localdb/localdb.go +++ b/internal/localdb/localdb.go @@ -1088,7 +1088,9 @@ func (l *LocalDB) RepairPendingChanges() (int, []string, error) { return repaired, remainingErrors, nil } -// repairProjectChange validates and fixes project data +// repairProjectChange validates and fixes project data. +// Note: This only validates local data. Server-side conflicts (like duplicate code+variant) +// are handled by sync service layer with deduplication logic. func (l *LocalDB) repairProjectChange(change *PendingChange) error { project, err := l.GetProjectByUUID(change.EntityUUID) if err != nil { @@ -1123,6 +1125,20 @@ func (l *LocalDB) repairProjectChange(change *PendingChange) error { modified = true } + // Check for local duplicates with same (code, variant) + var duplicate LocalProject + err = l.db.Where("code = ? AND variant = ? AND uuid != ?", project.Code, project.Variant, project.UUID). + First(&duplicate).Error + if err == nil { + // Found local duplicate - deduplicate by appending UUID suffix to variant + if project.Variant == "" { + project.Variant = project.UUID[:8] + } else { + project.Variant = project.Variant + "-" + project.UUID[:8] + } + modified = true + } + if modified { if err := l.SaveProject(project); err != nil { return fmt.Errorf("saving repaired project: %w", err) diff --git a/internal/services/sync/service.go b/internal/services/sync/service.go index 95480e5..24882c6 100644 --- a/internal/services/sync/service.go +++ b/internal/services/sync/service.go @@ -856,10 +856,29 @@ func (s *Service) pushProjectChange(change *localdb.PendingChange) error { } } - if err := projectRepo.UpsertByUUID(&project); err != nil { - return fmt.Errorf("upsert project on server: %w", err) + // Try upsert by UUID first + err = projectRepo.UpsertByUUID(&project) + if err != nil { + // Check if it's a duplicate (code, variant) constraint violation + // In this case, find existing project with same (code, variant) and link to it + var existing models.Project + lookupErr := mariaDB.Where("code = ? AND variant = ?", project.Code, project.Variant).First(&existing).Error + if lookupErr == nil { + // Found duplicate - link local project to existing server project + slog.Info("project duplicate found, linking to existing", + "local_uuid", project.UUID, + "server_uuid", existing.UUID, + "server_id", existing.ID, + "code", project.Code, + "variant", project.Variant) + project.ID = existing.ID + } else { + // Not a duplicate issue, return original error + return fmt.Errorf("upsert project on server: %w", err) + } } + // Update local project with server ID localProject, localErr := s.localDB.GetProjectByUUID(project.UUID) if localErr == nil { if project.ID > 0 { diff --git a/web/templates/base.html b/web/templates/base.html index 2140686..535d826 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -45,10 +45,10 @@
-