Skip to content

refactor: extract shared write flow UX from writer pages (#96)#120

Merged
sjordan0228 merged 4 commits into
devfrom
feature/shared-write-flow
Apr 6, 2026
Merged

refactor: extract shared write flow UX from writer pages (#96)#120
sjordan0228 merged 4 commits into
devfrom
feature/shared-write-flow

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Apr 6, 2026

Summary

  • Extracts duplicated write flow logic from all 4 writer pages into shared functions in SharedJS.h
  • sharedWriteFlow(config) — configurable write/verify/enrich pipeline
  • setupReadButton(config) — shared Read/Cancel toggle with tag polling
  • saveEnrichmentToSpoolman(uid, config) — shared Spoolman enrichment save
  • waitForTag(), enrichmentHasData(), setVal() — shared helpers
  • Net reduction: 476 lines (-920, +444), saves ~19KB flash

Changes

  • SharedJS.h — 5 new shared functions added
  • OpenSpoolWriterHTML.h — replaced ~240 lines with config-based calls
  • TigerTagWriterHTML.h — replaced ~286 lines with config-based calls
  • OpenTag3DWriterHTML.h — replaced ~290 lines with config-based calls
  • OpenPrintTagWriterHTML.h — replaced ~208 lines with config-based calls (including format step support)

Test plan

  • OpenSpool writer: write tag, verify, enrichment save
  • TigerTag writer: write tag, verify with material/brand ID check, enrichment save
  • OpenTag3D writer: write tag, verify, enrichment save
  • OpenPrintTag writer: format blank tag + write, verify via valuesMatch
  • Read button works on all 4 pages (polls, fills form, shows match badge)
  • Spoolman picker still works on all pages
  • Back/Another buttons return to form correctly

Summary by CodeRabbit

  • Refactor
    • Consolidated tag read/write UI into a shared pipeline across writer pages, simplifying flows, view toggles, and enrichment save hooks for more consistent user interactions.
  • New Features
    • Added LED support for an additional development board.
  • Bug Fixes
    • Improved spool lookup and weight-update semantics when saving enrichment to reduce incorrect weight calculations and mismatches.

Moves duplicated write flow logic (waitForTag, writeFlow, read button,
enrichment save) into SharedJS.h. Each writer page now calls
sharedWriteFlow() with a config object. Saves ~19KB flash.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e56e9e90-c649-44a4-91ff-80ea643146e7

📥 Commits

Reviewing files that changed from the base of the PR and between 11f26fd and b4b91bb.

📒 Files selected for processing (4)
  • src/OpenSpoolWriterHTML.h
  • src/OpenTag3DWriterHTML.h
  • src/SharedJS.h
  • src/TigerTagWriterHTML.h

📝 Walkthrough

Walkthrough

Extracts per-page tag read/write/enrichment flows into a shared, configurable client-side library (SharedJS.h), updates all writer pages to call the shared helpers, adds enrichment-save utilities, adjusts server-side spool lookup/weight semantics, and adds BOARD_S3_DEVKITC LED support.

Changes

Cohort / File(s) Summary
Shared workflow library
src/SharedJS.h
Added waitForTag(timeoutMs), sharedWriteFlow(config), setupReadButton(config), enrichmentHasData(fieldIds), setVal(id,val), and saveEnrichmentToSpoolman(uid, config) — centralizes tag polling, write/verify pipeline, read-button wiring, and enrichment persistence.
Writer HTML pages
src/OpenPrintTagWriterHTML.h, src/OpenSpoolWriterHTML.h, src/OpenTag3DWriterHTML.h, src/TigerTagWriterHTML.h
Removed page-local DOM caching, polling loops, write/read step state machines, and enrichment helper implementations; replaced with calls to sharedWriteFlow({...}), setupReadButton({...}), and saveEnrichmentToSpoolman(...); simplified view toggles and event wiring to use direct DOM lookups.
Backend spool enrichment handling
src/WebServerManager.cpp
Replaced extra_field=nfc_id query with paginated spool list lookup (limit=200) and exact extra.nfc_id matching, track initial_weight for matched spools, switch to updating/creating initial_weight and used_weight (replacing remaining_weight semantics), and switched to DynamicJsonDocument(16384).
LED configuration
src/LEDManager.cpp
Added BOARD_S3_DEVKITC branch to initialize _pixel as NEO_GRB in constructor and set NEO_GRB + NEO_KHZ800 in begin(pin).

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser (UI)
    participant Server as Server (API)
    participant Spoolman as Spoolman (External)

    Browser->>Server: GET /api/status (poll)  // waitForTag
    Server-->>Browser: {present, uid, tag_kind, status...}
    alt format check required
        Browser->>Server: POST /api/format-check {uid}
        Server-->>Browser: format result
    end
    Browser->>Server: POST /api/write... {payload}  // sharedWriteFlow -> write endpoint
    Server-->>Browser: write accepted (uid)
    loop verify loop (sharedWriteFlow)
        Browser->>Server: GET /api/status
        Server-->>Browser: status (verify condition)
    end
    Browser->>Server: POST /api/spoolman/save-enrichment {fields}  // saveEnrichmentToSpoolman
    Server->>Spoolman: GET /spools?limit=200
    Spoolman-->>Server: spool list
    alt existing spool found
        Server->>Spoolman: PATCH spool (initial_weight/used_weight)
    else create new spool
        Server->>Spoolman: POST spool (initial_weight/used_weight)
    end
    Spoolman-->>Server: save result
    Server-->>Browser: final status/result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: extract shared write flow UX from writer pages (#96)' accurately summarizes the main change: extracting duplicated write flow logic from multiple writer pages into a shared implementation.
Description check ✅ Passed The PR description covers all required sections: a clear Summary of the refactoring, detailed Changes listing affected files, and a comprehensive Test plan with specific verification steps for all affected pages.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/shared-write-flow

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/XL Extra large change (500+ lines) label Apr 6, 2026
The DevKitC's GPIO 48 WS2812B uses GRB ordering, not RGB like the
S3-Zero. Red was displaying as green.
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: 6

🤖 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/OpenSpoolWriterHTML.h`:
- Around line 284-289: The verify function currently only checks tag_kind ===
'OpenSpoolTag' which allows false positives; update the verify implementation
inside the sharedWriteFlow call to fetch the expected values from buildPayload
(or the payload passed to the write step) and explicitly compare the
OpenSpoolTag's fields (e.g., brand, material, color_hex and temperature fields
such as temp_hotend/temp_bed or whatever names are used in the payload) against
those expected values, returning true only if all those fields match the
payload; keep the tag_kind check but add equality checks for each required field
to ensure the written tag actually reflects the new OpenSpool data.

In `@src/OpenTag3DWriterHTML.h`:
- Around line 511-516: The current verify predicate passed into sharedWriteFlow
only checks status.tag_kind === 'OpenTag3D', which allows pre-existing tags to
falsely pass; update the verify function (the verify: ... passed alongside
endpoint '/api/write-opentag3d' and buildPayload) to perform a deep/content
check of the returned status against the actual write payload (compare required
properties from buildPayload such as the tag's unique id/name, geometry/mesh
fields, and any metadata/attributes that must match), e.g., derive the
expectedPayload from buildPayload and assert equality or presence of those
fields in status so the poll only succeeds when the read-back matches the
intended OpenTag3D payload.
- Around line 577-603: The fillForm function is populating decoded tag fields
(print_temp_c, bed_temp_c, density, min/max temps, etc.) then dispatching an
input on '#base_material', which triggers ot3dAutoFill to overwrite those values
because '#material_modifiers' wasn’t synced first; update fillForm so it either
(a) dispatches an input on the material modifiers first (element id
'material_modifiers') and waits for any change handlers to run before
dispatching on 'base_material', or (b) temporarily mark the fields as
"filled-from-tag" (e.g., set a data-auto-filled flag on the inputs) and clear
that flag after material modifier sync so ot3dAutoFill will not override those
values; implement the chosen approach inside fillForm and ensure ot3dAutoFill
checks that flag (or that material modifiers are synced) before applying
defaults.

In `@src/SharedJS.h`:
- Around line 527-531: The shared waitForTag() function currently hard-codes
"NTAG" in the user-facing instruction (via setResult('resultBox', 'Place and
hold an NTAG tag on the scanner.', '')) which is incorrect for other flows like
ISO15693; update waitForTag to accept a tagType (or optional message) parameter
and use that parameter to compose a generic/injection-safe instruction (e.g.,
"Place and hold a [tagType] tag on the scanner." or a neutral "Place and hold a
tag on the scanner.") and update all callers (e.g., any pages invoking
waitForTag) to pass the appropriate tag type so setResult and setBanner use the
correct text; keep function name waitForTag and the UI setters (setResult,
setBanner, setStepState) unchanged aside from the message change.

In `@src/TigerTagWriterHTML.h`:
- Around line 559-568: The verify predicate passed to sharedWriteFlow only
checks identity fields (material_id and brand_id) on status.tigertag versus
payload, so edits to mutable fields (e.g., weight, color, diameter,
temperature(s)) will be missed; update the verify function in the
sharedWriteFlow call to also compare the mutable fields on status.tigertag
(e.g., tt.weight, tt.color, tt.diameter, tt.temperature or temperature_* as
used) against the corresponding properties on payload and return false if any
mismatch is found, keeping the existing tag_kind and tigertag existence checks.
- Around line 625-643: The fillForm callback currently calls syncMaterialId()
which can overwrite the decoded tt.material_id and never restores
tt.diameter_mm/diameter_id; instead preserve and apply the parsed IDs directly:
set document.getElementById('material_id').value = tt.material_id and set the
diameter element (e.g., 'diameter_id' or the appropriate diameter input) from
tt.diameter_mm/tt.diameter_id if present, and remove or defer the
syncMaterialId() call (or add a guarded flag to syncMaterialId to skip when a
parsed material_id is present) so the async material list does not replace the
decoded values in fillForm.
🪄 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: 5f02c61f-69ea-424f-95e2-cb7780ed4c35

📥 Commits

Reviewing files that changed from the base of the PR and between f2b45bb and 014f8b1.

📒 Files selected for processing (5)
  • src/OpenPrintTagWriterHTML.h
  • src/OpenSpoolWriterHTML.h
  • src/OpenTag3DWriterHTML.h
  • src/SharedJS.h
  • src/TigerTagWriterHTML.h

Comment thread src/OpenSpoolWriterHTML.h Outdated
Comment on lines +284 to +289
sharedWriteFlow({
stepIds: ['step-wait', 'step-detect', 'step-write', 'step-verify'],
endpoint: '/api/write-openspool',
formatName: 'OpenSpool',
buildPayload: buildPayload,
verify: function(status) { return status.tag_kind === 'OpenSpoolTag'; },
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 | 🔴 Critical

OpenSpool verification needs to compare written fields.

Right now any present OpenSpoolTag satisfies verify(). If the tag already contains OpenSpool data, the loop can report success without proving that brand, material, color_hex, or temperatures changed.

💡 Suggested fix
-        verify: function(status) { return status.tag_kind === 'OpenSpoolTag'; },
+        verify: function(status, payload) {
+          if (status.tag_kind !== 'OpenSpoolTag' || !status.openspool) return false;
+          var os = status.openspool;
+          return String(os.brand || '') === String(payload.brand)
+              && String(os.material || '') === String(payload.type)
+              && String((os.color_hex || '').replace(/^#/, '').toUpperCase()) === String(payload.color_hex).toUpperCase()
+              && Number(os.min_temp || 0) === Number(payload.min_temp)
+              && Number(os.max_temp || 0) === Number(payload.max_temp);
+        },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenSpoolWriterHTML.h` around lines 284 - 289, The verify function
currently only checks tag_kind === 'OpenSpoolTag' which allows false positives;
update the verify implementation inside the sharedWriteFlow call to fetch the
expected values from buildPayload (or the payload passed to the write step) and
explicitly compare the OpenSpoolTag's fields (e.g., brand, material, color_hex
and temperature fields such as temp_hotend/temp_bed or whatever names are used
in the payload) against those expected values, returning true only if all those
fields match the payload; keep the tag_kind check but add equality checks for
each required field to ensure the written tag actually reflects the new
OpenSpool data.

Comment thread src/OpenTag3DWriterHTML.h Outdated
Comment thread src/OpenTag3DWriterHTML.h
Comment on lines +577 to +603
fillForm: function(status) {
var ot = status.opentag3d || {};
setVal('base_material', ot.base_material || '');
setVal('manufacturer', ot.manufacturer || '');
if (ot.color_hex) {
var c = ot.color_hex.startsWith('#') ? ot.color_hex : '#' + ot.color_hex;
setVal('colorHex', c);
setVal('colorPicker', c);
}
if (ot.target_weight_g) setVal('target_weight_g', ot.target_weight_g);
if (ot.density) setVal('density', ot.density);
if (ot.print_temp) setVal('print_temp_c', ot.print_temp);
if (ot.bed_temp) setVal('bed_temp_c', ot.bed_temp);
if (ot.min_print_temp) setVal('min_print_temp_c', ot.min_print_temp);
if (ot.max_print_temp) setVal('max_print_temp_c', ot.max_print_temp);
if (ot.min_bed_temp) setVal('min_bed_temp_c', ot.min_bed_temp);
if (ot.max_bed_temp) setVal('max_bed_temp_c', ot.max_bed_temp);
if (ot.dry_temp) setVal('max_dry_temp_c', ot.dry_temp);
if (ot.dry_time_hours) setVal('dry_time_hours', ot.dry_time_hours);
if (ot.diameter_mm) {
var dEl = document.getElementById('diameter_um');
if (dEl) dEl.value = Math.round(ot.diameter_mm * 1000);
}
setBanner('statusBanner', 'Tag written \u2713 Spoolman enrichment saved \u2713');
} catch(e) {
setBanner('statusBanner', 'Tag written \u2713 Spoolman save failed \u2014 check connection');
if (ot.color_name) setVal('color_name', ot.color_name);
var matEl = document.getElementById('base_material');
if (matEl) matEl.dispatchEvent(new Event('input'));
},
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

Don’t let ot3dAutoFill() overwrite the values you just read.

This callback fills print_temp_c, bed_temp_c, density, min/max temps, etc., then dispatches input on #base_material. Because those fields still count as auto-filled, ot3dAutoFill() can immediately replace the decoded tag values with material DB defaults — and it can do so with the wrong modifier profile because #material_modifiers is never synced first.

💡 Suggested fix
       fillForm: function(status) {
         var ot = status.opentag3d || {};
         setVal('base_material', ot.base_material || '');
+        setVal('material_modifiers', ot.modifiers || '');
         setVal('manufacturer', ot.manufacturer || '');
@@
         if (ot.color_name) setVal('color_name', ot.color_name);
+        ['print_temp_c','bed_temp_c','min_print_temp_c','max_print_temp_c','min_bed_temp_c','max_bed_temp_c','density','max_dry_temp_c','dry_time_hours']
+          .forEach(function(id) {
+            var el = document.getElementById(id);
+            if (el && el.value !== '') el.dataset.autoFilled = 'false';
+          });
         var matEl = document.getElementById('base_material');
         if (matEl) matEl.dispatchEvent(new Event('input'));
       },
📝 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
fillForm: function(status) {
var ot = status.opentag3d || {};
setVal('base_material', ot.base_material || '');
setVal('manufacturer', ot.manufacturer || '');
if (ot.color_hex) {
var c = ot.color_hex.startsWith('#') ? ot.color_hex : '#' + ot.color_hex;
setVal('colorHex', c);
setVal('colorPicker', c);
}
if (ot.target_weight_g) setVal('target_weight_g', ot.target_weight_g);
if (ot.density) setVal('density', ot.density);
if (ot.print_temp) setVal('print_temp_c', ot.print_temp);
if (ot.bed_temp) setVal('bed_temp_c', ot.bed_temp);
if (ot.min_print_temp) setVal('min_print_temp_c', ot.min_print_temp);
if (ot.max_print_temp) setVal('max_print_temp_c', ot.max_print_temp);
if (ot.min_bed_temp) setVal('min_bed_temp_c', ot.min_bed_temp);
if (ot.max_bed_temp) setVal('max_bed_temp_c', ot.max_bed_temp);
if (ot.dry_temp) setVal('max_dry_temp_c', ot.dry_temp);
if (ot.dry_time_hours) setVal('dry_time_hours', ot.dry_time_hours);
if (ot.diameter_mm) {
var dEl = document.getElementById('diameter_um');
if (dEl) dEl.value = Math.round(ot.diameter_mm * 1000);
}
setBanner('statusBanner', 'Tag written \u2713 Spoolman enrichment saved \u2713');
} catch(e) {
setBanner('statusBanner', 'Tag written \u2713 Spoolman save failed \u2014 check connection');
if (ot.color_name) setVal('color_name', ot.color_name);
var matEl = document.getElementById('base_material');
if (matEl) matEl.dispatchEvent(new Event('input'));
},
fillForm: function(status) {
var ot = status.opentag3d || {};
setVal('base_material', ot.base_material || '');
setVal('material_modifiers', ot.modifiers || '');
setVal('manufacturer', ot.manufacturer || '');
if (ot.color_hex) {
var c = ot.color_hex.startsWith('#') ? ot.color_hex : '#' + ot.color_hex;
setVal('colorHex', c);
setVal('colorPicker', c);
}
if (ot.target_weight_g) setVal('target_weight_g', ot.target_weight_g);
if (ot.density) setVal('density', ot.density);
if (ot.print_temp) setVal('print_temp_c', ot.print_temp);
if (ot.bed_temp) setVal('bed_temp_c', ot.bed_temp);
if (ot.min_print_temp) setVal('min_print_temp_c', ot.min_print_temp);
if (ot.max_print_temp) setVal('max_print_temp_c', ot.max_print_temp);
if (ot.min_bed_temp) setVal('min_bed_temp_c', ot.min_bed_temp);
if (ot.max_bed_temp) setVal('max_bed_temp_c', ot.max_bed_temp);
if (ot.dry_temp) setVal('max_dry_temp_c', ot.dry_temp);
if (ot.dry_time_hours) setVal('dry_time_hours', ot.dry_time_hours);
if (ot.diameter_mm) {
var dEl = document.getElementById('diameter_um');
if (dEl) dEl.value = Math.round(ot.diameter_mm * 1000);
}
if (ot.color_name) setVal('color_name', ot.color_name);
['print_temp_c','bed_temp_c','min_print_temp_c','max_print_temp_c','min_bed_temp_c','max_bed_temp_c','density','max_dry_temp_c','dry_time_hours']
.forEach(function(id) {
var el = document.getElementById(id);
if (el && el.value !== '') el.dataset.autoFilled = 'false';
});
var matEl = document.getElementById('base_material');
if (matEl) matEl.dispatchEvent(new Event('input'));
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenTag3DWriterHTML.h` around lines 577 - 603, The fillForm function is
populating decoded tag fields (print_temp_c, bed_temp_c, density, min/max temps,
etc.) then dispatching an input on '#base_material', which triggers ot3dAutoFill
to overwrite those values because '#material_modifiers' wasn’t synced first;
update fillForm so it either (a) dispatches an input on the material modifiers
first (element id 'material_modifiers') and waits for any change handlers to run
before dispatching on 'base_material', or (b) temporarily mark the fields as
"filled-from-tag" (e.g., set a data-auto-filled flag on the inputs) and clear
that flag after material modifier sync so ot3dAutoFill will not override those
values; implement the chosen approach inside fillForm and ensure ot3dAutoFill
checks that flag (or that material modifiers are synced) before applying
defaults.

Comment thread src/SharedJS.h Outdated
Comment on lines +527 to +531
async function waitForTag(timeoutMs) {
var deadline = Date.now() + timeoutMs;
setStepState('step-wait', 'active');
setBanner('statusBanner', 'Waiting for tag\u2026');
setResult('resultBox', 'Place and hold an NTAG tag on the scanner.', '');
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

Don’t hard-code NTAG instructions in the shared wait state.

waitForTag() now drives every writer page, but it still hard-codes “NTAG”. src/OpenPrintTagWriterHTML.h is an ISO15693 flow, so this shared copy now tells users to present the wrong tag type before the write even starts.

💡 Suggested fix
-async function waitForTag(timeoutMs) {
+async function waitForTag(timeoutMs, waitPrompt) {
   var deadline = Date.now() + timeoutMs;
   setStepState('step-wait', 'active');
   setBanner('statusBanner', 'Waiting for tag…');
-  setResult('resultBox', 'Place and hold an NTAG tag on the scanner.', '');
+  setResult('resultBox', waitPrompt || 'Place and hold a tag on the scanner.', '');
@@
-    var presentStatus = await waitForTag(8000);
+    var presentStatus = await waitForTag(8000, config.waitPrompt);

Also applies to: 560-560

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

In `@src/SharedJS.h` around lines 527 - 531, The shared waitForTag() function
currently hard-codes "NTAG" in the user-facing instruction (via
setResult('resultBox', 'Place and hold an NTAG tag on the scanner.', '')) which
is incorrect for other flows like ISO15693; update waitForTag to accept a
tagType (or optional message) parameter and use that parameter to compose a
generic/injection-safe instruction (e.g., "Place and hold a [tagType] tag on the
scanner." or a neutral "Place and hold a tag on the scanner.") and update all
callers (e.g., any pages invoking waitForTag) to pass the appropriate tag type
so setResult and setBanner use the correct text; keep function name waitForTag
and the UI setters (setResult, setBanner, setStepState) unchanged aside from the
message change.

Comment thread src/TigerTagWriterHTML.h
Comment on lines +559 to +568
sharedWriteFlow({
stepIds: ['step-wait', 'step-detect', 'step-write', 'step-verify'],
endpoint: '/api/write-tigertag',
formatName: 'TigerTag',
buildPayload: buildPayload,
verify: function(status, payload) {
if (status.tag_kind !== 'TigerTag' || !status.tigertag) return false;
var tt = status.tigertag;
return tt.material_id === payload.material_id && tt.brand_id === payload.brand_id;
},
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 | 🔴 Critical

Verify a mutable TigerTag field too.

material_id + brand_id is only spool identity. If the user edits weight, color, diameter, or temperatures on the same TigerTag, this predicate still returns true and sharedWriteFlow() reports a verified write even when those bytes did not change.

💡 Suggested fix
         verify: function(status, payload) {
           if (status.tag_kind !== 'TigerTag' || !status.tigertag) return false;
           var tt = status.tigertag;
-          return tt.material_id === payload.material_id && tt.brand_id === payload.brand_id;
+          return tt.material_id === payload.material_id
+              && tt.brand_id === payload.brand_id
+              && Number(tt.weight_g) === Number(payload.weight_g)
+              && Number(tt.nozzle_temp_min || 0) === Number(payload.nozzle_min)
+              && Number(tt.nozzle_temp_max || 0) === Number(payload.nozzle_max);
         },
📝 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
sharedWriteFlow({
stepIds: ['step-wait', 'step-detect', 'step-write', 'step-verify'],
endpoint: '/api/write-tigertag',
formatName: 'TigerTag',
buildPayload: buildPayload,
verify: function(status, payload) {
if (status.tag_kind !== 'TigerTag' || !status.tigertag) return false;
var tt = status.tigertag;
return tt.material_id === payload.material_id && tt.brand_id === payload.brand_id;
},
sharedWriteFlow({
stepIds: ['step-wait', 'step-detect', 'step-write', 'step-verify'],
endpoint: '/api/write-tigertag',
formatName: 'TigerTag',
buildPayload: buildPayload,
verify: function(status, payload) {
if (status.tag_kind !== 'TigerTag' || !status.tigertag) return false;
var tt = status.tigertag;
return tt.material_id === payload.material_id
&& tt.brand_id === payload.brand_id
&& Number(tt.weight_g) === Number(payload.weight_g)
&& Number(tt.nozzle_temp_min || 0) === Number(payload.nozzle_min)
&& Number(tt.nozzle_temp_max || 0) === Number(payload.nozzle_max);
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/TigerTagWriterHTML.h` around lines 559 - 568, The verify predicate passed
to sharedWriteFlow only checks identity fields (material_id and brand_id) on
status.tigertag versus payload, so edits to mutable fields (e.g., weight, color,
diameter, temperature(s)) will be missed; update the verify function in the
sharedWriteFlow call to also compare the mutable fields on status.tigertag
(e.g., tt.weight, tt.color, tt.diameter, tt.temperature or temperature_* as
used) against the corresponding properties on payload and return false if any
mismatch is found, keeping the existing tag_kind and tigertag existence checks.

Comment thread src/TigerTagWriterHTML.h
Comment on lines +625 to +643
fillForm: function(status) {
var tt = status.tigertag || {};
setVal('material_search', tt.material_name || '');
setVal('brand_name', tt.brand_name || '');
if (tt.color_hex) {
var c = tt.color_hex.startsWith('#') ? tt.color_hex : '#' + tt.color_hex;
setVal('colorHex', c);
setVal('colorPicker', c);
}
setBanner('statusBanner', 'Tag written \u2713 Spoolman enrichment saved \u2713');
} catch(e) {
setBanner('statusBanner', 'Tag written \u2713 Spoolman save failed \u2014 check connection');
if (tt.weight_g) setVal('weight_g', tt.weight_g);
if (tt.nozzle_temp_min) setVal('nozzle_min', tt.nozzle_temp_min);
if (tt.nozzle_temp_max) setVal('nozzle_max', tt.nozzle_temp_max);
if (tt.bed_temp_min) setVal('bed_min', tt.bed_temp_min);
if (tt.bed_temp_max) setVal('bed_max', tt.bed_temp_max);
if (tt.dry_temp) setVal('dry_temp', tt.dry_temp);
if (tt.dry_time_hours) setVal('dry_time', tt.dry_time_hours);
if (tt.material_id !== undefined) document.getElementById('material_id').value = tt.material_id;
if (tt.brand_id !== undefined) document.getElementById('brand_id').value = tt.brand_id;
syncMaterialId();
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 the parsed TigerTag IDs and diameter instead of re-deriving them.

This callback already has exact tt.material_id and tt.diameter_mm from the decoded tag. Calling syncMaterialId() afterward can overwrite the parsed ID with the default PLA mapping when the async material list has not finished loading, and diameter_id is never restored at all.

💡 Suggested fix
       fillForm: function(status) {
         var tt = status.tigertag || {};
         setVal('material_search', tt.material_name || '');
         setVal('brand_name', tt.brand_name || '');
@@
         if (tt.dry_temp) setVal('dry_temp', tt.dry_temp);
         if (tt.dry_time_hours) setVal('dry_time', tt.dry_time_hours);
+        if (tt.diameter_mm) setVal('diameter_id', tt.diameter_mm >= 2.0 ? '221' : '56');
         if (tt.material_id !== undefined) document.getElementById('material_id').value = tt.material_id;
         if (tt.brand_id !== undefined) document.getElementById('brand_id').value = tt.brand_id;
-        syncMaterialId();
+        if (tt.material_id === undefined) syncMaterialId();
       },
📝 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
fillForm: function(status) {
var tt = status.tigertag || {};
setVal('material_search', tt.material_name || '');
setVal('brand_name', tt.brand_name || '');
if (tt.color_hex) {
var c = tt.color_hex.startsWith('#') ? tt.color_hex : '#' + tt.color_hex;
setVal('colorHex', c);
setVal('colorPicker', c);
}
setBanner('statusBanner', 'Tag written \u2713 Spoolman enrichment saved \u2713');
} catch(e) {
setBanner('statusBanner', 'Tag written \u2713 Spoolman save failed \u2014 check connection');
if (tt.weight_g) setVal('weight_g', tt.weight_g);
if (tt.nozzle_temp_min) setVal('nozzle_min', tt.nozzle_temp_min);
if (tt.nozzle_temp_max) setVal('nozzle_max', tt.nozzle_temp_max);
if (tt.bed_temp_min) setVal('bed_min', tt.bed_temp_min);
if (tt.bed_temp_max) setVal('bed_max', tt.bed_temp_max);
if (tt.dry_temp) setVal('dry_temp', tt.dry_temp);
if (tt.dry_time_hours) setVal('dry_time', tt.dry_time_hours);
if (tt.material_id !== undefined) document.getElementById('material_id').value = tt.material_id;
if (tt.brand_id !== undefined) document.getElementById('brand_id').value = tt.brand_id;
syncMaterialId();
fillForm: function(status) {
var tt = status.tigertag || {};
setVal('material_search', tt.material_name || '');
setVal('brand_name', tt.brand_name || '');
if (tt.color_hex) {
var c = tt.color_hex.startsWith('#') ? tt.color_hex : '#' + tt.color_hex;
setVal('colorHex', c);
setVal('colorPicker', c);
}
if (tt.weight_g) setVal('weight_g', tt.weight_g);
if (tt.nozzle_temp_min) setVal('nozzle_min', tt.nozzle_temp_min);
if (tt.nozzle_temp_max) setVal('nozzle_max', tt.nozzle_temp_max);
if (tt.bed_temp_min) setVal('bed_min', tt.bed_temp_min);
if (tt.bed_temp_max) setVal('bed_max', tt.bed_temp_max);
if (tt.dry_temp) setVal('dry_temp', tt.dry_temp);
if (tt.dry_time_hours) setVal('dry_time', tt.dry_time_hours);
if (tt.diameter_mm) setVal('diameter_id', tt.diameter_mm >= 2.0 ? '221' : '56');
if (tt.material_id !== undefined) document.getElementById('material_id').value = tt.material_id;
if (tt.brand_id !== undefined) document.getElementById('brand_id').value = tt.brand_id;
if (tt.material_id === undefined) syncMaterialId();
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/TigerTagWriterHTML.h` around lines 625 - 643, The fillForm callback
currently calls syncMaterialId() which can overwrite the decoded tt.material_id
and never restores tt.diameter_mm/diameter_id; instead preserve and apply the
parsed IDs directly: set document.getElementById('material_id').value =
tt.material_id and set the diameter element (e.g., 'diameter_id' or the
appropriate diameter input) from tt.diameter_mm/tt.diameter_id if present, and
remove or defer the syncMaterialId() call (or add a guarded flag to
syncMaterialId to skip when a parsed material_id is present) so the async
material list does not replace the decoded values in fillForm.

- Spoolman uses used_weight not remaining_weight; send initial_weight
  and calculated used_weight instead
- Spool lookup by nfc_id was broken (extra_field filter unreliable);
  now fetches all spools and matches nfc_id client-side, skipping
  archived spools
- Add 500ms delay before enrichment save to avoid connection reset
  from concurrent ESP32 requests
- OpenSpool verify now checks brand, material, and color
- OpenTag3D verify now checks base_material, manufacturer, and weight
- TigerTag verify now also checks weight_g
- waitForTag() no longer says "NTAG" (OpenPrintTag uses ISO15693)
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: 5

🤖 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/SharedJS.h`:
- Around line 690-694: enrichmentHasData currently treats "0" as no data because
it checks parseFloat(el.value) > 0; change it to detect whether a value was
provided instead of requiring strictly positive numbers: in enrichmentHasData
(and the similar check around the other occurrence noted) check that the element
exists and el.value is not an empty string (e.g., el.value.trim() !== '') and
consider the field as present even if the parsed number is 0 or other falsy
numeric values; also ensure you still guard against non-numeric input by
validating parseFloat/el.value is a valid number (isFinite) where the calling
code expects numeric content.
- Around line 533-540: The polling loops call api('/api/status') and allow
rejections to escape; update both loops (the one using
setStepState('step-wait','done') and the similar loop around api('/api/status')
at the other location) to catch transient errors and treat them as a poll miss:
wrap the await api('/api/status') in try/catch, on error log/ignore it (matching
setupReadButton() transient handling), await sleep(500) and continue rather than
throwing, so a short-lived 503/reset won't fail the whole flow; apply the same
change to the second occurrence.
- Around line 724-726: The client currently sends filament_id = -2 when the user
declines a matched filament, but
WebServerManager::handleApiSpoolmanSaveEnrichment() only treats -1 as
"search/create" and treats other negatives as failures; update the server to
recognize -2 as "skip lookup/create" (i.e., if filament_id == -2, do not attempt
findOrCreate and leave the filament unset/unchanged) and ensure the code path
does not return "filament create failed"; alternatively, if you prefer a
client-only fix, keep the decline sentinel local by not sending filament_id at
all (or send a distinct null/absent value) from the client code that sets
filamentId (the block using fr and fconfirmed) so the server sees no filament_id
and skips lookup/create—apply the same change for the other occurrence
referenced near the fr handling.
- Around line 596-607: In sharedWriteFlow's verification loop, don't treat any
matching tag as success—ensure the verified tag UID matches the originally
detected tag: change the condition that currently uses config.verify(status,
payload) to also require the same UID (e.g., compare status.uid or
status.present.uid to the earlier detected presentStatus.uid or payload.uid).
Update the while-loop condition in sharedWriteFlow (the block using
api('/api/status'), config.verify, setStepState, and afterSuccess) so success is
only declared when status.present && config.verify(...) && status.uid ===
<originalDetectedUid>.

In `@src/WebServerManager.cpp`:
- Around line 1952-1970: The current lookup sets spoolId==-1 on any GET/JSON
failure or when only the first page (?limit=200) is checked, causing create
logic to run prematurely; change the lookup so it paginates (e.g., add
offset/page param in the URL and loop calls to http.GET()/deserializeJson) and
only attempt to create a new spool when every page was successfully fetched and
searched without finding a matching quotedUid (use the same variables: baseUrl,
url, http.GET(), response, deserializeJson into sDoc, JsonArray arr, and compare
arr[i]["extra"]["nfc_id"] to quotedUid); also treat any HTTP or JSON error as a
failed lookup and do NOT proceed to create—exit the lookup loop and leave
spoolId==-1 until a full successful exhaustion confirms absence.
🪄 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: e9e75c63-9285-4253-8edd-05e50f5e64b3

📥 Commits

Reviewing files that changed from the base of the PR and between 014f8b1 and 11f26fd.

📒 Files selected for processing (3)
  • src/LEDManager.cpp
  • src/SharedJS.h
  • src/WebServerManager.cpp

Comment thread src/SharedJS.h
Comment on lines +533 to +540
while (Date.now() < deadline) {
var status = await api('/api/status');
if (status.present) {
setStepState('step-wait', 'done');
return status;
}
await sleep(500);
}
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

Retry transient /api/status failures instead of failing the whole flow.

Both polling loops let api('/api/status') rejections escape, so one short-lived 503/reset turns a recoverable poll miss into a hard write failure. setupReadButton() already treats the same failure mode as transient.

💡 Suggested change
   while (Date.now() < deadline) {
-    var status = await api('/api/status');
-    if (status.present) {
-      setStepState('step-wait', 'done');
-      return status;
-    }
+    try {
+      var status = await api('/api/status');
+      if (status.present) {
+        setStepState('step-wait', 'done');
+        return status;
+      }
+    } catch (e) {
+      // retry until timeout
+    }
     await sleep(500);
   }
@@
     while (Date.now() < verifyDeadline) {
       await sleep(500);
-      var status = await api('/api/status');
+      var status;
+      try {
+        status = await api('/api/status');
+      } catch (e) {
+        continue;
+      }
       if (status.present && config.verify(status, payload)) {

Also applies to: 596-600

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

In `@src/SharedJS.h` around lines 533 - 540, The polling loops call
api('/api/status') and allow rejections to escape; update both loops (the one
using setStepState('step-wait','done') and the similar loop around
api('/api/status') at the other location) to catch transient errors and treat
them as a poll miss: wrap the await api('/api/status') in try/catch, on error
log/ignore it (matching setupReadButton() transient handling), await sleep(500)
and continue rather than throwing, so a short-lived 503/reset won't fail the
whole flow; apply the same change to the second occurrence.

Comment thread src/SharedJS.h
Comment on lines +596 to +607
var verifyDeadline = Date.now() + 15000;
while (Date.now() < verifyDeadline) {
await sleep(500);
var status = await api('/api/status');
if (status.present && config.verify(status, payload)) {
setBanner('statusBanner', 'Tag verified \u2014 hold for a moment\u2026');
await sleep(2000);
setStepState('step-verify', 'done');
setBanner('statusBanner', 'Write complete \u2014 safe to remove tag.');
setResult('resultBox', config.formatName + ' tag written and verified successfully.', 'success');
await sleep(500);
if (config.afterSuccess) await config.afterSuccess(presentStatus.uid);
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

Require the same UID during verification.

sharedWriteFlow() already knows which tag was detected, but success is declared on any config.verify(...) match. The current OpenSpool and OpenTag3D call sites only verify tag_kind, so swapping to another same-format tag can still show “verified”.

💡 Suggested change
-      if (status.present && config.verify(status, payload)) {
+      if (status.present &&
+          status.uid === presentStatus.uid &&
+          config.verify(status, payload)) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/SharedJS.h` around lines 596 - 607, In sharedWriteFlow's verification
loop, don't treat any matching tag as success—ensure the verified tag UID
matches the originally detected tag: change the condition that currently uses
config.verify(status, payload) to also require the same UID (e.g., compare
status.uid or status.present.uid to the earlier detected presentStatus.uid or
payload.uid). Update the while-loop condition in sharedWriteFlow (the block
using api('/api/status'), config.verify, setStepState, and afterSuccess) so
success is only declared when status.present && config.verify(...) && status.uid
=== <originalDetectedUid>.

Comment thread src/SharedJS.h
Comment on lines +690 to +694
function enrichmentHasData(fieldIds) {
return fieldIds.some(function(id) {
var el = document.getElementById(id);
return el && el.value && parseFloat(el.value) > 0;
});
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

Preserve remaining_g = 0 end-to-end.

enrichmentHasData() rejects 0 as “no data”, and the request body later collapses falsy values back to 0. That makes an empty spool impossible to represent unambiguously through this path. Track whether the field was provided instead of using strict-positive/falsy coercion.

Also applies to: 740-740

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

In `@src/SharedJS.h` around lines 690 - 694, enrichmentHasData currently treats
"0" as no data because it checks parseFloat(el.value) > 0; change it to detect
whether a value was provided instead of requiring strictly positive numbers: in
enrichmentHasData (and the similar check around the other occurrence noted)
check that the element exists and el.value is not an empty string (e.g.,
el.value.trim() !== '') and consider the field as present even if the parsed
number is 0 or other falsy numeric values; also ensure you still guard against
non-numeric input by validating parseFloat/el.value is a valid number (isFinite)
where the calling code expects numeric content.

Comment thread src/SharedJS.h
Comment on lines +724 to +726
if (fr.found) {
var fconfirmed = confirm('Found existing filament "' + fr.name + '" in Spoolman. Use it?');
filamentId = fconfirmed ? fr.id : -2;
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

Declining a matched filament currently guarantees a failed save.

This sends filament_id = -2 on “No”, but WebServerManager::handleApiSpoolmanSaveEnrichment() only searches/creates when filament_id == -1; any other negative value falls through to filament create failed. Keep the decline sentinel client-local, or teach the server to treat -2 as “skip lookup and create”.

Also applies to: 746-746

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

In `@src/SharedJS.h` around lines 724 - 726, The client currently sends
filament_id = -2 when the user declines a matched filament, but
WebServerManager::handleApiSpoolmanSaveEnrichment() only treats -1 as
"search/create" and treats other negatives as failures; update the server to
recognize -2 as "skip lookup/create" (i.e., if filament_id == -2, do not attempt
findOrCreate and leave the filament unset/unchanged) and ensure the code path
does not return "filament create failed"; alternatively, if you prefer a
client-only fix, keep the decline sentinel local by not sending filament_id at
all (or send a distinct null/absent value) from the client code that sets
filamentId (the block using fr and fconfirmed) so the server sees no filament_id
and skips lookup/create—apply the same change for the other occurrence
referenced near the fr handling.

Comment thread src/WebServerManager.cpp
Comment on lines +1952 to +1970
snprintf(url, sizeof(url), "%s/api/v1/spool?limit=200", baseUrl);
http.begin(client, url);
http.setTimeout(5000);
code = http.GET();
if (code == 200) {
response = http.getString();
StaticJsonDocument<2048> sDoc;
DynamicJsonDocument sDoc(16384);
if (!deserializeJson(sDoc, response)) {
JsonArray arr = sDoc.as<JsonArray>();
if (arr.size() > 0) spoolId = arr[0]["id"] | -1;
for (size_t i = 0; i < arr.size(); i++) {
const char* nfcId = arr[i]["extra"]["nfc_id"] | "";
if (strcmp(nfcId, quotedUid) == 0) {
bool archived = arr[i]["archived"] | false;
if (archived) continue;
spoolId = arr[i]["id"] | -1;
existingInitialWeight = arr[i]["initial_weight"] | 0.0f;
break;
}
}
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

Don't create a new spool unless the UID lookup actually exhausted the result set.

This only inspects ?limit=200, and any GET/JSON failure also leaves spoolId == -1. The create branch then runs anyway, so a larger Spoolman instance or transient lookup failure can duplicate an already-linked NFC ID.

Also applies to: 1999-2025

🧰 Tools
🪛 Clang (14.0.6)

[note] 1956-1956: +1, including nesting penalty of 0, nesting level increased to 1

(clang)


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

(clang)


[warning] 1958-1958: variable 'sDoc' is not initialized

(cppcoreguidelines-init-variables)


[warning] 1960-1960: variable 'arr' 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 1952 - 1970, The current lookup sets
spoolId==-1 on any GET/JSON failure or when only the first page (?limit=200) is
checked, causing create logic to run prematurely; change the lookup so it
paginates (e.g., add offset/page param in the URL and loop calls to
http.GET()/deserializeJson) and only attempt to create a new spool when every
page was successfully fetched and searched without finding a matching quotedUid
(use the same variables: baseUrl, url, http.GET(), response, deserializeJson
into sDoc, JsonArray arr, and compare arr[i]["extra"]["nfc_id"] to quotedUid);
also treat any HTTP or JSON error as a failed lookup and do NOT proceed to
create—exit the lookup loop and leave spoolId==-1 until a full successful
exhaustion confirms absence.

@sjordan0228 sjordan0228 merged commit 819ded0 into dev Apr 6, 2026
1 of 2 checks passed
@sjordan0228 sjordan0228 deleted the feature/shared-write-flow branch April 21, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XL Extra large change (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant