Skip to content

[NRT-591] Surface duplicate anki_id suggestion error + redesign bulk suggestion summary#1306

Draft
RisingOrange wants to merge 4 commits into
mainfrom
worktree-nrt-591-duplicate-anki-id
Draft

[NRT-591] Surface duplicate anki_id suggestion error + redesign bulk suggestion summary#1306
RisingOrange wants to merge 4 commits into
mainfrom
worktree-nrt-591-duplicate-anki-id

Conversation

@RisingOrange
Copy link
Copy Markdown
Collaborator

@RisingOrange RisingOrange commented Jun 2, 2026

Related issues

Proposed changes

When a user submits a new-note suggestion for a note whose anki_id already exists in the deck on AnkiHub (note created + auto-accepted, then edited and re-suggested before a sync), the server rejects it with "A deck can't contain multiple notes with the same anki_id.". Previously this failure was effectively hidden (single → raw showInfo; bulk → an uncategorized blob in the plain-text summary). This PR surfaces it and lets the user recover.

Detection (main/suggestions.py)

  • ANKIHUB_DUPLICATE_ANKI_ID_ERROR constant + parse_duplicate_anki_id_error() — tolerates the backend's DRF-normalized payload (list-wrapped/stringified: reads conflicting_ankihub_id[0], compares conflicting_note_deleted[0] == "True", not truthiness).
  • Resubmit core: _new_note_to_change_suggestion() converts the already-built NewNoteSuggestion into a ChangeNoteSuggestion keyed by the conflicting ah_nid and lets the server diff — no remote fetch, no AnkiHub-DB write. resubmit_new_note_as_change_suggestion() (single) and resubmit_new_notes_as_change_suggestions_in_bulk() (one batched call).
  • suggest_notes_in_bulk now returns already_in_deck_by_nid on the result; soft-deleted conflicts are routed to the existing deleted-on-AnkiHub category instead (a change suggestion would 404 a tombstoned note).

Scenario 1 — single suggestion (gui/suggestion_dialog.py)

  • New NoteAlreadyExistsDialog (info icon, Cancel / Send as change suggestion). The suggestion dialog closes and the modal shows over the editor; on success → toast; soft-deleted conflict → existing "deleted from AnkiHub" dialog; missing conflicting id (older server) → falls back to today's generic error.

Scenario 2 — bulk suggestion summary (gui/suggestion_dialog.py)

  • Replaces the plain-text showText summary with a real BulkSuggestionSummaryDialog, shown for all outcomes (full redesign): colored counts, bold (N) category titles, a new "Other errors" bucket with a community.ankihub.net/c/support link, and the redundant "All notes with failed suggestions" block removed.
  • New interactive "Notes already in this deck" action box: Resubmit (one batched create_suggestions_in_bulk call) / Ignore; Close/X disabled until resolved; "Updated" badges on changed categories; a hard resubmit failure re-enables Close (no dead-end).

Analytics (structlog → Datadog): error_dialog_shown, resubmit_clicked, bulk_error_category_shown, bulk_resubmit_clicked.

How to reproduce

As a maintainer on staging:

  1. Create a new note in Anki using one of your deck's AnkiHub note types and Suggest it (auto-accepts so the note is created on AnkiHub).
  2. Don't sync. Edit the same note and suggest it again.
  3. Before: the failure was hidden (single: a raw error; bulk: an uncategorized failure count).
  4. After: single → "Note already exists in this deck" dialog with Send as change suggestion; bulk → the note lands in the "Notes already in this deck" action box with a Resubmit as change suggestions button.

Tests: pytest tests/addon -k "BulkSuggestionSummaryDialog or MaybeHandleNoteAlreadyExists or ParseDuplicate or NewNoteToChange or OnSuggestNotesInBulkDone or already_in_deck".

Screenshots and videos

All dialog states (single modal, bulk default / loading / success / ignored) were verified live against the real add-on code. Design reference: the Figma + mockups on NRT-591. (Can add inline screenshots on request.)

Further comments

  • Resubmit avoids any local AnkiHub-DB write — it converts the failed new-note suggestion directly, so there's no half-synced state; trade-off is that editing the same note again before a sync could re-trigger the error (a normal sync reconciles it).
  • Root-cause "persist the generated ankihub_id on accept" was deliberately rejected — it would couple the add-on to synchronous server-side note creation, which may change; dedup belongs server-side.
  • Open design question (not blocking): per-category note IDs currently render as truncated lists (first 25, … and N more). A "Copy note IDs" button (copying an Anki Browse nid:… search) is proposed and pending designer confirmation.
  • Built as a separate branch off main, not stacked on the auto-protect flag cleanup ([NRT-771] Post-flag-flip cleanup: drop AUTO_PROTECT_FEATURE_FLAG scaffolding + thread NoteDiff #1305) which touches the same two files; the overlap is mechanical, not architectural.

…summary

Single suggestion: when a new-note suggestion is rejected because the note
already exists in the deck on AnkiHub, show a 'Note already exists in this deck'
dialog offering to resubmit the edits as a change suggestion (converting the
already-built NewNoteSuggestion, no remote fetch / AnkiHub-DB write). Soft-deleted
conflicts route to the existing deleted-on-AnkiHub handling.

Bulk suggestion: replace the plain-text showText summary with a real dialog shown
for all outcomes - categorized results, a new 'Other errors' bucket with a support
link, and an interactive 'Notes already in this deck' action (batched resubmit /
ignore) with Close gating and 'Updated' badges. Degrades gracefully when the
backend doesn't return the conflicting id.

Adds analytics events (error_dialog_shown, resubmit_clicked,
bulk_error_category_shown, bulk_resubmit_clicked) and tests.
- Gate X/Escape (reject()/closeEvent()) on the bulk summary dialog so an
  unresolved action or in-flight resubmit can't be dismissed - previously only
  the Close button was disabled. Adds a regression test.
- Replace the stringly-typed action state + separate hard-error flag with a
  single _ActionState enum (a hard resubmit failure is now FAILED: retry
  buttons stay, Close works).
- Reuse gui.utils.clear_layout instead of a local duplicate; add missing
  type annotations.
…icate-anki-id

# Conflicts:
#	ankihub/gui/suggestion_dialog.py
#	ankihub/main/suggestions.py
- Move the bulk summary dialog, its action-state machine, categories, color
  helpers, and the bulk-submit callback to gui/bulk_suggestion_summary_dialog.py.
- Move the shared panel color helpers to gui/utils.py (avoids a circular
  import between the two dialog modules).
- Drop the change_type plumbing from the bulk callback/dialog - converted
  resubmits are always 'Updated content', matching the single-note path.
- Flatten the nested resubmit task/on_done callbacks in
  _show_note_already_exists_dialog.
- Rename module constants to the no-underscore convention.
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.

1 participant