Skip to content

fix: prevent duplicate label creation from Android sync engine#310

Merged
dkhalife merged 2 commits intomainfrom
fix/label-duplication-bug
Apr 26, 2026
Merged

fix: prevent duplicate label creation from Android sync engine#310
dkhalife merged 2 commits intomainfrom
fix/label-duplication-bug

Conversation

@dkhalife
Copy link
Copy Markdown
Owner

Problem

Creating a label from the Android app resulted in many duplicate copies being created.

Root Cause

Response format mismatch (primary): The server's CreateLabel endpoint returned {"label": {"id": ..., "name": ..., "color": ...}} (a full object), but the Android client's LabelCreatedResponse expected {"label": 123} (just an integer ID, matching the task creation pattern). Gson failed to deserialize the nested object as an Int, so response.body()?.label was null, the outbox recorded a failure ("Empty create response"), and retried on every sync cycle -- each retry creating another copy on the server.

No uniqueness enforcement (secondary): There was no unique constraint on (created_by, name) in the labels table and no server-side duplicate check, so every retry succeeded in creating another label.

Changes

Server (apiserver)

  • Fix response format: CreateLabel now returns {"label": id} (just the ID), consistent with task creation. The WebSocket broadcast still sends the full label object for real-time sync.
  • Migration 008: Deduplicates existing labels (keeping lowest ID, reassigning task_labels), then adds unique index on labels(created_by, name). Supports both SQLite and MySQL.
  • Duplicate checking: CreateLabel and UpdateLabel now check for existing labels with the same name (per user) before insert/update, returning HTTP 409 Conflict. DB constraint violations are also caught and mapped to 409.
  • GORM model: Updated Label struct tags with uniqueIndex for documentation.

Frontend

  • Updated CreateLabel API to use LabelIdResponse ({label: number}) and reconstruct the full label from request data + returned ID in the Redux slice (matching the existing task creation pattern).

The Android app's LabelCreatedResponse expects {label: int} but the
server returned {label: {id, name, color}}. Gson failed to deserialize
the object as an Int, causing the outbox to record a failure and retry
on every sync cycle — each retry creating another copy on the server.

Changes:
- Fix CreateLabel response to return just the label ID (consistent
  with task creation pattern)
- Update frontend to handle ID-only create response, reconstructing
  the label from request data + returned ID
- Add migration 008: deduplicate existing labels and add unique
  constraint on (created_by, name)
- Add server-side duplicate checking in CreateLabel and UpdateLabel,
  returning HTTP 409 on conflict
- Add isDuplicateKeyError helper to catch DB constraint violations
  as 409 instead of 500

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 26, 2026 14:57
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 26, 2026

Codecov Report

❌ Patch coverage is 5.60748% with 101 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rver/internal/migrations/008_unique_label_names.go 0.00% 68 Missing ⚠️
apiserver/internal/services/labels/label.go 0.00% 31 Missing ⚠️
apiserver/internal/repos/label/label.go 75.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes duplicate label creation caused by a client/server response-shape mismatch during label creation, and adds server-side + DB-level enforcement to prevent same-user duplicate label names.

Changes:

  • Align label creation API response to return { label: id } (matching task creation), and update the frontend to reconstruct the label object from the request + returned id.
  • Add server-side duplicate-name checks for label create/update and map duplicates to HTTP 409 Conflict (including DB-constraint violations).
  • Add migration 008 to deduplicate existing labels and introduce a unique index on (created_by, name) for both SQLite and MySQL.
Show a summary per file
File Description
frontend/src/store/labelsSlice.ts Reconstructs label object from returned label id during create, matching existing task-create pattern.
frontend/src/api/labels.ts Updates CreateLabel HTTP transport typing to expect { label: number }.
apiserver/internal/services/labels/label.go Adds duplicate checks + 409 mapping; changes CreateLabel response format to { label: id }.
apiserver/internal/repos/label/label.go Adds LabelExistsByName helper used by the service layer.
apiserver/internal/models/label.go Documents uniqueness intention via GORM tags for (created_by, name).
apiserver/internal/migrations/008_unique_label_names.go Deduplicates existing labels and adds unique index on (created_by, name) across SQLite/MySQL.

Copilot's findings

  • Files reviewed: 6/6 changed files
  • Comments generated: 3

Comment thread apiserver/internal/migrations/008_unique_label_names.go Outdated
Comment thread apiserver/internal/services/labels/label.go
Comment thread apiserver/internal/repos/label/label.go
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dkhalife dkhalife merged commit aeb604d into main Apr 26, 2026
7 of 8 checks passed
@dkhalife dkhalife deleted the fix/label-duplication-bug branch April 26, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants