Skip to content

fix: register-uid deduplicates filaments and spools#136

Merged
sjordan0228 merged 3 commits into
devfrom
feature/register-uid-dedup
Apr 10, 2026
Merged

fix: register-uid deduplicates filaments and spools#136
sjordan0228 merged 3 commits into
devfrom
feature/register-uid-dedup

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Apr 9, 2026

Summary

  • Replace unconditional filament POST with enrichFindOrCreateFilament() — searches by vendor + material + color before creating
  • Replace unconditional spool POST with enrichFindSpoolByUid() check — if spool with same nfc_id exists, PATCH it via enrichUpdateSpool() instead of creating a duplicate
  • Net reduction of ~20 lines by reusing existing helpers

Root Cause

handleApiRegisterUid() was the only Spoolman write path that didn't check for existing entries. Every web UI tag write created new filament + spool entries unconditionally, causing duplicates when:

  1. A tag was written twice
  2. A tag write was followed by a reader scan (triggering SpoolmanManager sync)
  3. A failed write was retried

Test plan

  • Write a tag via web UI — verify spool created in Spoolman
  • Write same tag again — verify existing spool updated, no duplicate
  • Read the tag — verify SpoolmanManager sync finds the existing spool
  • Write tag with different filament — verify new filament found or created correctly

Closes #134, closes #135
Related: #130

Summary by CodeRabbit

  • Refactor
    • Spool registration now checks for an existing spool by unique identifier and updates it when found; new spools are only created when needed.
    • Filament lookup/creation flow consolidated with clearer failure messaging and improved logging of the resolved filament ID.
    • NFC write handling adjusted to avoid unnecessary reactivation when a session is active, reducing redundant operations and preserving retry behavior on failures.

Replace unconditional POST with find-or-create using existing helpers:
- enrichFindOrCreateFilament searches by vendor+material+color first
- enrichFindSpoolByUid checks for existing spool by nfc_id before creating
- Existing spools are PATCHed via enrichUpdateSpool instead of duplicated

Likely root cause of community-reported spool duplication (#130).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

Warning

Rate limit exceeded

@sjordan0228 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 27 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 27 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 07ae2362-fe89-4bdd-882a-7699cbd76f7e

📥 Commits

Reviewing files that changed from the base of the PR and between 1801457 and 38a9238.

📒 Files selected for processing (1)
  • src/SharedJS.h
📝 Walkthrough

Walkthrough

Replaced inline filament creation and unconditional spool creation in handleApiRegisterUid() with calls to enrichment helpers: enrichFindOrCreateFilament(), enrichFindSpoolByUid(), and enrichUpdateSpool(). Adjusted failure messages and added logging of resolved filamentId; preserved fallback spool creation when no match found.

Changes

Cohort / File(s) Summary
WebServerManager: register-uid flow
src/WebServerManager.cpp
Replaced direct POST-based filament creation with enrichFindOrCreateFilament(). Replaced unconditional spool POST with enrichFindSpoolByUid() lookup; if a spool exists, call enrichUpdateSpool() (computes effective initial_weight), otherwise keep local POST creation fallback. Updated error messages and added filamentId log.
NFC tag write session behavior
src/HardwareNFCConnection.cpp
writeISO14443Pages() now reactivates ISO14443A only when tagSessionActive_ is false; skips RF setup when session is active and retains per-page retry/halt/reactivation logic.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WebServerManager
    participant Helper as Enrich Helpers
    participant Spoolman as Spoolman API

    Client->>WebServerManager: handleApiRegisterUid(uid, filamentData, spoolData)

    rect rgba(100, 150, 200, 0.5)
    Note over WebServerManager,Helper: Filament resolution
    WebServerManager->>Helper: enrichFindOrCreateFilament(filamentData)
    Helper->>Spoolman: GET /api/v1/filament?query
    alt found
        Spoolman-->>Helper: filament {id}
    else not found
        Helper->>Spoolman: POST /api/v1/filament
        Spoolman-->>Helper: filament {id}
    end
    Helper-->>WebServerManager: filamentId
    end

    rect rgba(150, 100, 200, 0.5)
    Note over WebServerManager,Helper: Spool resolution
    WebServerManager->>Helper: enrichFindSpoolByUid(nfc_id)
    Helper->>Spoolman: GET /api/v1/spool?query
    alt found
        Spoolman-->>Helper: spool {id, weight}
        Helper->>Helper: enrichUpdateSpool(spoolId, mergedData)
        Helper->>Spoolman: PATCH /api/v1/spool/{id}
        Spoolman-->>Helper: updated {id}
    else not found
        Helper->>Spoolman: POST /api/v1/spool
        Spoolman-->>Helper: spool {id}
    end
    Helper-->>WebServerManager: spoolId
    end

    WebServerManager->>Client: 200 {spoolId, filamentId}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: deduplicating filaments and spools in the register-uid flow by implementing find-or-create patterns.
Description check ✅ Passed The description covers all required sections: Summary, Changes (with bullet points), How to Test (with checklist items), and standard testing requirements. It is comprehensive and addresses the root cause.
Linked Issues check ✅ Passed The code changes directly address the objectives from both linked issues: filament deduplication via enrichFindOrCreateFilament() [#134] and spool deduplication via enrichFindSpoolByUid() and enrichUpdateSpool() [#135].
Out of Scope Changes check ✅ Passed Changes are scoped to the register-uid flow in WebServerManager::handleApiRegisterUid() and a conditional optimization in HardwareNFCConnection::writeISO14443Pages(), both aligned with the deduplication objective.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/register-uid-dedup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/M Medium change (50-200 lines) label Apr 9, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/WebServerManager.cpp`:
- Around line 394-420: The inline spool creation in the register-uid path sets
remaining_weight directly which is inconsistent with
enrichCreateSpool()/enrichUpdateSpool() that derive used_weight and set an
initial_weight (avoiding direct remaining_weight); update the register-uid flow
to call enrichCreateSpool() (or extend enrichCreateSpool() to accept an optional
initialWeight and pass the user-provided initialWeight) so the created JSON uses
initial_weight and used_weight semantics (matching spoolman expectations)
instead of setting remaining_weight directly; ensure you remove the direct
remaining_weight write in the inline block and preserve quotedUid in the
extra.nfc_id field when delegating to enrichCreateSpool().
- Around line 416-418: The current code builds a JSON error body by
concatenating response into errMsg and calling _server.send which can produce
invalid JSON if response contains quotes/special chars; replace that custom JSON
construction with the existing sendError() helper (or call _server.send with a
properly escaped/JSON-encoded response) so the HTTP error is returned safely and
consistently—locate the block using errMsg, xSemaphoreGive, and _server.send and
change it to use sendError() (or a JSON-encoding routine) while keeping the
existing log of the full Spoolman response for debugging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 93c5e47a-331b-43f9-be44-baf724a9b7e9

📥 Commits

Reviewing files that changed from the base of the PR and between 015585d and 6e7fff8.

📒 Files selected for processing (1)
  • src/WebServerManager.cpp

Comment thread src/WebServerManager.cpp
Comment on lines +394 to +420
} else {
// No existing spool — create new
StaticJsonDocument<512> spoolBody;
spoolBody["filament_id"] = filamentId;
if (initialWeight > 0) spoolBody["initial_weight"] = initialWeight;
if (remainingWeight > 0) spoolBody["remaining_weight"] = remainingWeight;

JsonObject extra = spoolBody.createNestedObject("extra");
extra["nfc_id"] = quotedUid;

String spoolJson;
serializeJson(spoolBody, spoolJson);
Serial.printf("register-uid: creating spool: %s\n", spoolJson.c_str());

snprintf(url, sizeof(url), "%s/api/v1/spool", baseUrl);
http.begin(client, url);
http.addHeader("Content-Type", "application/json");
code = http.POST(spoolJson);
if (code != 200 && code != 201) {
response = http.getString();
http.end();
Serial.printf("register-uid: spool creation failed HTTP %d: %s\n", code, response.c_str());
String errMsg = "Failed to create spool (HTTP " + String(code) + "): " + response;
xSemaphoreGive(g_httpMutex);
_server.send(500, "application/json", "{\"error\":\"" + errMsg + "\"}");
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent weight handling between inline creation and helper function.

The inline spool creation path sets remaining_weight directly (line 399), while enrichCreateSpool() (lines 1954-1959) and enrichUpdateSpool() (lines 1931-1937) convert remaining weight to used_weight with an explicit initial_weight. Spoolman computes remaining_weight = initial_weight - used_weight, so setting remaining_weight directly may behave differently or be less reliable.

Consider reusing enrichCreateSpool() here for consistency:

Proposed refactor to use enrichCreateSpool()
     } else {
         // No existing spool — create new
-        StaticJsonDocument<512> spoolBody;
-        spoolBody["filament_id"] = filamentId;
-        if (initialWeight > 0) spoolBody["initial_weight"] = initialWeight;
-        if (remainingWeight > 0) spoolBody["remaining_weight"] = remainingWeight;
-
-        JsonObject extra = spoolBody.createNestedObject("extra");
-        extra["nfc_id"] = quotedUid;
-
-        String spoolJson;
-        serializeJson(spoolBody, spoolJson);
-        Serial.printf("register-uid: creating spool: %s\n", spoolJson.c_str());
-
-        snprintf(url, sizeof(url), "%s/api/v1/spool", baseUrl);
-        http.begin(client, url);
-        http.addHeader("Content-Type", "application/json");
-        code = http.POST(spoolJson);
-        if (code != 200 && code != 201) {
-            response = http.getString();
-            http.end();
-            Serial.printf("register-uid: spool creation failed HTTP %d: %s\n", code, response.c_str());
-            String errMsg = "Failed to create spool (HTTP " + String(code) + "): " + response;
-            xSemaphoreGive(g_httpMutex);
-            _server.send(500, "application/json", "{\"error\":\"" + errMsg + "\"}");
-            return;
-        }
-        response = http.getString();
-        http.end();
-
-        StaticJsonDocument<1024> spoolDoc;
-        if (!deserializeJson(spoolDoc, response)) {
-            spoolId = spoolDoc["id"] | -1;
-        }
+        Serial.printf("register-uid: creating spool for uid=%s\n", uid);
+        spoolId = enrichCreateSpool(client, http, baseUrl, filamentId, remainingWeight, quotedUid);
+        if (spoolId < 0) {
+            xSemaphoreGive(g_httpMutex);
+            sendError(500, "Failed to create spool");
+            return;
+        }
     }

Note: enrichCreateSpool() uses a hardcoded 1000g initial weight when remainingG > 0. If preserving the user-provided initialWeight is important, you may need to extend enrichCreateSpool() to accept an optional initial weight parameter.

🧰 Tools
🪛 Clang (14.0.6)

[note] 394-394: +1, nesting level increased to 1

(clang)


[note] 412-412: +2, including nesting penalty of 1, nesting level increased to 2

(clang)


[note] 412-412: +1

(clang)


[warning] 396-396: variable 'spoolBody' is not initialized

(cppcoreguidelines-init-variables)


[warning] 401-401: variable 'extra' is not initialized

(cppcoreguidelines-init-variables)


[warning] 404-404: variable 'spoolJson' is not initialized

(cppcoreguidelines-init-variables)


[warning] 416-416: variable 'errMsg' is not initialized

(cppcoreguidelines-init-variables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WebServerManager.cpp` around lines 394 - 420, The inline spool creation
in the register-uid path sets remaining_weight directly which is inconsistent
with enrichCreateSpool()/enrichUpdateSpool() that derive used_weight and set an
initial_weight (avoiding direct remaining_weight); update the register-uid flow
to call enrichCreateSpool() (or extend enrichCreateSpool() to accept an optional
initialWeight and pass the user-provided initialWeight) so the created JSON uses
initial_weight and used_weight semantics (matching spoolman expectations)
instead of setting remaining_weight directly; ensure you remove the direct
remaining_weight write in the inline block and preserve quotedUid in the
extra.nfc_id field when delegating to enrichCreateSpool().

Comment thread src/WebServerManager.cpp
Comment on lines +416 to +418
String errMsg = "Failed to create spool (HTTP " + String(code) + "): " + response;
xSemaphoreGive(g_httpMutex);
_server.send(500, "application/json", "{\"error\":\"" + errMsg + "\"}");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unescaped error message can produce malformed JSON.

The response from Spoolman may contain quotes or special characters. Concatenating it directly into the JSON string bypasses the escaping logic in sendError(), potentially producing invalid JSON responses.

Proposed fix using sendError()
         if (code != 200 && code != 201) {
             response = http.getString();
             http.end();
             Serial.printf("register-uid: spool creation failed HTTP %d: %s\n", code, response.c_str());
-            String errMsg = "Failed to create spool (HTTP " + String(code) + "): " + response;
             xSemaphoreGive(g_httpMutex);
-            _server.send(500, "application/json", "{\"error\":\"" + errMsg + "\"}");
+            char errMsg[128];
+            snprintf(errMsg, sizeof(errMsg), "Failed to create spool (HTTP %d)", code);
+            sendError(500, errMsg);
             return;
         }

This truncates the Spoolman response detail but ensures valid JSON. Alternatively, use sendError() with just the HTTP code and log the full response for debugging (which is already done on line 415).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
String errMsg = "Failed to create spool (HTTP " + String(code) + "): " + response;
xSemaphoreGive(g_httpMutex);
_server.send(500, "application/json", "{\"error\":\"" + errMsg + "\"}");
xSemaphoreGive(g_httpMutex);
char errMsg[128];
snprintf(errMsg, sizeof(errMsg), "Failed to create spool (HTTP %d)", code);
sendError(500, errMsg);
🧰 Tools
🪛 Clang (14.0.6)

[warning] 416-416: variable 'errMsg' is not initialized

(cppcoreguidelines-init-variables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WebServerManager.cpp` around lines 416 - 418, The current code builds a
JSON error body by concatenating response into errMsg and calling _server.send
which can produce invalid JSON if response contains quotes/special chars;
replace that custom JSON construction with the existing sendError() helper (or
call _server.send with a properly escaped/JSON-encoded response) so the HTTP
error is returned safely and consistently—locate the block using errMsg,
xSemaphoreGive, and _server.send and change it to use sendError() (or a
JSON-encoding routine) while keeping the existing log of the full Spoolman
response for debugging.

Same pattern as readISO14443Pages — check tagSessionActive_ before
calling setupRF() + activateTypeA(). The scan loop's detectTag()
leaves the tag active, but writeISO14443Pages was unconditionally
resetting the RF field, killing the session and failing to reactivate.

This fixes write failures when a tag is already on the reader and
a write is queued from the web UI.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/HardwareNFCConnection.cpp`:
- Around line 377-384: The write flow in writeISO14443Pages() leaves
tagSessionActive_ set even when the code halts the tag, causing subsequent calls
to skip reactivation; update writeISO14443Pages() (and any helper that issues
HALT) to clear tagSessionActive_ (set it false) immediately after performing
iso14443a_->halt()/halting logic so the fast-path reactivation code (which
checks tagSessionActive_) behaves correctly on the next call; ensure any
early-return paths after halting also clear tagSessionActive_ before returning.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 3bfd69dd-f11a-408b-94c9-f6142603a449

📥 Commits

Reviewing files that changed from the base of the PR and between 6e7fff8 and 1801457.

📒 Files selected for processing (1)
  • src/HardwareNFCConnection.cpp

Comment on lines +377 to +384
if (!tagSessionActive_) {
// Tag was halted — must reactivate before writing
iso14443a_->setupRF();
uidLen = iso14443a_->activateTypeA(response, 1);
if (uidLen < 4) {
Serial.println("HardwareNFC: writeISO14443Pages - tag reactivation failed");
return false;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep tagSessionActive_ in sync with HALT in write flow.

This new fast-path depends on tagSessionActive_, but writeISO14443Pages() halts at Line 414/Line 421 without clearing the flag. That can make the next call skip activation incorrectly and fall into avoidable retries/failures.

🔧 Proposed fix
     if (!tagSessionActive_) {
         // Tag was halted — must reactivate before writing
         iso14443a_->setupRF();
         uidLen = iso14443a_->activateTypeA(response, 1);
         if (uidLen < 4) {
             Serial.println("HardwareNFC: writeISO14443Pages - tag reactivation failed");
             return false;
         }
+        tagSessionActive_ = true;
     }
@@
                 uidLen = iso14443a_->activateTypeA(response, 1);
                 if (uidLen < 4) {
                     Serial.printf("HardwareNFC: writeISO14443Pages - re-activation failed on retry %d\n", retry + 1);
                     continue;
                 }
+                tagSessionActive_ = true;
                 pageOk = iso14443a_->mifareBlockWrite4(page, data + (i * 4));
             }
@@
                 Serial.printf("HardwareNFC: writeISO14443Pages - FAILED at page %d after retries (%lums, %d retries total)\n",
                               page, elapsed, retryCount);
                 iso14443a_->mifareHalt();
+                tagSessionActive_ = false;
                 return false;  // total write failure; clean exit for caller to retry entire sequence
             }
@@
     unsigned long elapsed = millis() - writeStart;
     iso14443a_->mifareHalt();
+    tagSessionActive_ = false;
     Serial.printf("HardwareNFC: writeISO14443Pages - wrote %d pages starting at page %d (%lums, %d retries)\n",
                   pageCount, startPage, elapsed, retryCount);

Also applies to: 414-422

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HardwareNFCConnection.cpp` around lines 377 - 384, The write flow in
writeISO14443Pages() leaves tagSessionActive_ set even when the code halts the
tag, causing subsequent calls to skip reactivation; update writeISO14443Pages()
(and any helper that issues HALT) to clear tagSessionActive_ (set it false)
immediately after performing iso14443a_->halt()/halting logic so the fast-path
reactivation code (which checks tagSessionActive_) behaves correctly on the next
call; ensure any early-return paths after halting also clear tagSessionActive_
before returning.

@sjordan0228 sjordan0228 merged commit 6597523 into dev Apr 10, 2026
2 checks passed
@sjordan0228 sjordan0228 deleted the feature/register-uid-dedup branch April 10, 2026 00:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Medium change (50-200 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant