mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-05 13:41:36 +02:00
feat(currency): add Belize Dollars (BZD) to currency dropdown (#1454)
Adds BZD with symbol BZ$ to the Settings cost-currency picker so users in Belize can track filament costs in their local currency without doing 2:1 USD mental conversions.
This commit is contained in:
@@ -4,7 +4,12 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
|
||||
## [0.2.5b1] - Unreleased
|
||||
|
||||
### Added
|
||||
- **Currency: Belize Dollars (BZD) added to the Settings → Cost currency dropdown (#1454, requested by @PLGuerraDesigns)** — Reporter accurately tracks 3D-printing filament costs in his local currency and BZD wasn't selectable, forcing a manual 2:1 mental conversion from USD. Added `BZD: 'BZ$'` to `frontend/src/utils/currency.ts` next to MXN (Americas dollar-prefix grouping); `getCurrencySymbol('BZD')` returns `'BZ$'` and the SUPPORTED_CURRENCIES list now has 30 entries. Unit test added in `frontend/src/__tests__/utils/currency.test.ts` covering the symbol lookup and presence in SUPPORTED_CURRENCIES; entry-count assertion bumped to 30 so any future additions/removals are caught immediately. 14 currency tests green; frontend build clean.
|
||||
|
||||
### Fixed
|
||||
- **Scheduler: queue items with `force_color_match` filament overrides now produce a correct AMS mapping at dispatch (#1437, fixed by external PR #1440 from @Person2099)** — Contributor's own bug report and fix. He had a queue item with `filament_overrides: [{slot_id: 1, type: "PLA", color: "#CBC6B8", force_color_match: true}]` and `ams_mapping: null`, expecting Bambuddy to translate the override into a slot mapping at dispatch time. Instead the scheduler dispatched with `ams_mapping: null` and the P1S fell back to type-only AMS matching, picking the wrong-colour slot. **Two-layer root cause** he traced end-to-end. **(1) `backend/app/services/filament_requirements.py:69`**: `extract_filament_requirements(file_path, plate_id=None)` fell through to `_collect_filaments(root, filaments)` whose XPath `./filament` only matches direct children of `<config>`. Modern BambuStudio 3MFs wrap filaments inside `<plate>` elements, so this XPath returned `[]` on every modern multi-plate 3MF when no specific plate was targeted — which is the standard scheduler call shape for queue items without a pinned plate. The downstream "no AMS mapping" cascade ALL flowed from this empty filament_reqs result. Fix walks `<plate>` elements first, dedupes by `slot_id` (highest `used_grams` wins on ties — sane because BambuStudio slots are project-wide and the entry that extruded the most is the most representative for AMS planning), and preserves the old `./filament` XPath as a fallback when no `<plate>` elements are present, so legacy 3MFs continue to parse unchanged. **(2) `backend/app/services/print_scheduler.py:792` — defence in depth**: even with (1) in place, edge cases exist where `_get_filament_requirements` can still return None (3MF missing `slice_info.config` entirely, IO failure during ZIP extraction, etc). New `_build_override_direct_mapping(force_overrides, status)` helper kicks in at exactly that moment when `force_color_match` overrides are present — builds the requirement list directly from the overrides (`slot_id`, `type`, `color`, empty `tray_info_idx`) and delegates to the existing `_match_filaments_to_slots()` cascade against the printer's loaded AMS state. Wrong-colour slot credit via the cascade's type-only fallback is impossible-by-construction because the upstream `_get_missing_force_color_slots()` printer-eligibility gate at `:590` already requires an exact `(type, normalised colour)` pair to be loaded *before* the printer is even considered for the job, so by the time `_build_override_direct_mapping` runs the exact match is guaranteed in the loaded set and the cascade's `exact_match` branch wins (colour normalisation is identical on both sides — `tray_color.replace("#", "").lower()[:6]`). Pref-only overrides (without `force_color_match`) intentionally do NOT trigger the fallback — they keep the pre-PR "no mapping, printer picks defaults" behaviour, so the new fallback is strictly opt-in via `force_color_match: true`. **Backwards-compat triple-checked**: legacy 3MF format unchanged (preserved fallback path); `plate_id != None` branch untouched (entire fix is inside the `else` of `if plate_id is not None`); `filament_overrides=None` / `[]` / no-force-entries all preserve the existing `return None` path; malformed JSON in `filament_overrides` is caught by the existing try/except, logged, and still returns None. **Tests** (22 new across two files; all pass on `pytest -n 30`): `backend/tests/unit/services/test_filament_requirements.py` — 4 tests covering the `plate_id=None` modern-format path, multi-plate collection, slot-dedup-by-highest-grams, and single-plate-modern-format. `backend/tests/unit/test_scheduler_force_color_ams_fallback.py` — 18 tests across `TestBuildOverrideDirectMapping` (single override matches AMS slot, empty AMS returns None, no colour match still produces a mapping length, multi-override produces multi-element mapping, external spool match yields global_tray_id 254, `tray_info_idx` is cleared) and `TestComputeAmsMappingFallback` (fallback used when reqs empty + force overrides present, fallback NOT used when no force_color flag, fallback NOT used when overrides None, normal path still used when reqs available, printer-status-unavailable returns None gracefully). 5079 backend tests + ruff + frontend build all clean post-merge; #1457/#1459/#1440 verified non-interacting (different services, different code paths, different timings). External-PR-checklist (per [[feedback_pr_changelog_required]]): contributor doesn't add CHANGELOG, this entry added by Martin post-merge.
|
||||
|
||||
- **Spoolman: per-print weight reporting now works for tag-less spools assigned via the Bambuddy UI (#1459, reported by @Moskito99 — follow-up to #1119)** — Reporter on Postgres + Postgres-backed Spoolman noticed that prints finished cleanly but the spool's remaining weight in Spoolman was never decremented. He correctly traced it: Spoolman's `extra.tag` on his spool was empty, and writing a value in there by hand made weight tracking start working. **Root cause** is one missing fallback path. After #1119 introduced the local `spoolman_slot_assignments` table as the authoritative binding for tag-less spools (RFID is the binding for Bambu Lab spools, slot-assignment is the binding for generic / non-RFID spools), the Assign UI deliberately leaves Spoolman's `extra.tag` field empty for those spools — and after the #1457 cleanup we now actively *clear* it on re-binding to stop ghost links resurfacing in the hover card. That's the correct write-side behaviour. But the per-print weight tracker (`backend/app/services/spoolman_tracking.py:_report_spool_usage_for_slots`) only resolved the bound spool via `client.find_spool_by_tag(spool_tag)` — a single tag-lookup against Spoolman's `extra.tag`. For tag-less spools that returns None and the tracker silently skipped the slot. The tracker **never consulted the local `spoolman_slot_assignments` table** that has the answer (verified: `grep -n SpoolmanSlotAssignment backend/app/services/spoolman_tracking.py` returned zero hits before this fix). So Bambu Lab RFID users got correct weight reporting (their `extra.tag` is auto-populated by the AMS-sync `create_spool` path at `backend/app/services/spoolman.py:1076`), and generic-spool users on Spoolman saw weight tracking silently no-op — exactly the symptom Moskito99 saw. **Fix** adds a two-stage resolver inside `_report_spool_usage_for_slots`: stage 1 is the existing `client.find_spool_by_tag(spool_tag)` (RFID and any RFID-equivalent `extra.tag` value), stage 2 is the new `_resolve_spool_id_via_slot_assignment(printer_id, ams_id, tray_id)` helper that queries the `SpoolmanSlotAssignment` table for `(printer_id, ams_id, tray_id) → spoolman_spool_id`. The (ams_id, tray_id) pair is derived from the slot's global_tray_id via the existing `_global_tray_id_to_ams_slot` helper — same translation used for fallback-tag generation, so external slots (global 254/255 → ams_id=255, tray_id=0/1) and AMS-HT slots (global 128+ → ams_id=global, tray_id=0) all resolve correctly. Stage-1-wins ordering is deliberate: when an RFID-bound spool is in the slot, `extra.tag` is the authoritative binding, even if the slot-assignment table happens to point at a different spool (legacy state). The resulting `[SPOOLMAN] … via tag` vs `… via slot-assignment` suffix in the success log makes it obvious which path resolved each slot, which support bundles will use to confirm the fix is live. `printer_id` threaded through the three callers (`_report_partial_usage` G-code path, `_report_partial_usage` linear path, `report_usage`) — they all already had `printer_id` in scope. Crucially, **`extra.tag` is NOT auto-populated** by this fix — that would re-introduce exactly the pollution #1457 cleaned up (deterministic fallback tags surviving across spool changes and surfacing stale spools in the hover card). The slot-assignment table is the source of truth for non-RFID bindings; Spoolman's `extra.tag` is reserved for hardware RFID identifiers. **Tests:** 5 new in `backend/tests/integration/test_spoolman_tracking_slot_fallback.py`: the bug repro (tag missing + slot-assignment present → use_spool by the slot-assignment's id); tag-match wins when both present (a regression that flips the resolution order would credit the wrong spool); skip-when-neither (no spool resolution attempted); skip-when-printer_id-not-supplied (legacy call shape stays inert); external-slot translation (global 254 → ams_id=255 tray_id=0 lookup works). New `patch_async_session` fixture routes the tracker's module-level `async_session` to the test engine so the in-test `SpoolmanSlotAssignment` insert is visible to the lookup. **Postgres compatibility:** verified — the lookup uses a plain `select(...where...).scalar_one_or_none()`, no SQLite-only syntax. 642 spoolman/tracking tests + 5 new = 647 green; full backend suite 5065 green; ruff clean.
|
||||
|
||||
- **Spoolman: AMS hover card and SpoolBuddy fill-bar no longer surface a stale spool after re-assigning a non-RFID slot (#1457, reported by @Menthe11)** — Reporter on a P1S with generic (non-RFID) PLA saw two different spools rendered in the AMS hover card: the top "Spulen-ID / Im Inventar öffnen" link pointed at an almost-empty black PLA spool that had been in the slot weeks earlier, while the bottom "Zugewiesen" block correctly showed the full spool the user had just assigned via Spoolman. **Root cause is two-layered.** For non-RFID slots Bambuddy falls back to a deterministic per-slot tag (`hash(printer_serial) + ams_id + tray_id`, 16 hex chars; see `frontend/src/utils/amsHelpers.ts:176`). When a user runs Link UI on such a slot, that fallback tag is written to the Spoolman spool's `extra.tag` — and the existing Link / Assign routes never cleared it from the previous holder when the user re-bound the slot to a different spool. The frontend's hover-card resolver at `frontend/src/pages/PrintersPage.tsx:3736` (and the matching sites at `:4137` / `:4452` for HT and external slots) then preferred that stale tag-link over the user's explicit slot-assignment: `linkedSpoolId: (trayTag ? linkedSpools?.[trayTag]?.id : undefined) ?? slotAssignmentForFill?.spoolman_spool_id`. So when both layers existed and they disagreed, the *stale* spool won, and FilamentHoverCard's dedupe at line 377 couldn't collapse the two buttons because the IDs didn't match → two "Im Inventar öffnen" buttons pointing at different spools. The SpoolBuddy AMS page had the identical bug shape in two more spots: `getSpoolmanFillForSlot()` (the per-slot fill-percentage resolver, line 138) walked tag-link before slot-assignment, so the fill bar reported the *old* spool's remaining grams instead of the freshly assigned full one; and the slot-action picker's "Linked spool" / "Assigned spool" branches (line 760) showed "Linked spool" whenever a tag-link existed, regardless of whether a (more recent) slot-assignment also existed. **Fix has two parts.** (1) Frontend precedence swap at all five sites: slot-assignment is the user's most explicit, most recent action — it must outrank the tag-link, which is auto-populated and can be silently stale. With the swap, FilamentHoverCard's existing match-dedupe collapses both buttons into one pointing at the correct spool; SpoolBuddy's fill bar reads from the assigned spool's weight first; and SpoolBuddy's slot-action picker drops the stale "Linked spool" line entirely when a slot-assignment exists. (2) Backend hygiene so the stale state is never written in the first place: a new `_clear_stale_tag_links(client, tag, keep_spool_id, log_context)` helper in `backend/app/api/routes/spoolman_inventory.py` enumerates Spoolman spools and PATCHes `extra.tag` to JSON-empty (`'""'`, the same wire shape `unlink_spool` already uses so the read-side `.strip('"')` filter in `get_linked_spools` skips it) on any spool *other than* the one being bound that still claims the same tag. Wired into `POST /spoolman/inventory/slot-assignments` (computes the slot's deterministic fallback tag via the existing `get_fallback_spool_tag_for_slot` helper in `spoolman_tracking.py` — newly promoted to a public symbol that mirrors the frontend's `getFallbackSpoolTag(serial, amsId, trayId)` signature) and `POST /spoolman/spools/{id}/link` (passes the literal `spool_tag` being bound — works for both RFID tags and fallback tags). Both are best-effort: per-spool patch failures and Spoolman enumeration failures are logged and skipped, never raised, so the assign/link path never wedges on a Spoolman hiccup. Existing assign-route tests stay green because their fixtures' Spoolman client mock already had `get_spools` returning `[]` (or now does — fixture updated in `test_spoolman_slot_assignments.py`, `test_spoolman_slot_concurrency.py`, `test_spoolman_slot_assignment_mqtt.py`, and the link-route test fixture in `test_spoolman_api.py`). **Tests** (8 new in `backend/tests/unit/test_spoolman_stale_tag_cleanup.py`): clears one other-spool while keeping the bound spool and unrelated-tag spool intact; case-insensitive match (the helper uppercases both sides because `get_linked_spools` already does); empty-tag short-circuits without enumerating spools; `keep_spool_id` guards against clearing the spool being bound; Spoolman 5xx during enumeration is swallowed and the call returns 0; one per-spool patch failure doesn't abort the rest of the cleanup; the slot-fallback wrapper computes the right tag and clears it; empty serial returns 0 without enumerating. Backend: ruff clean, 581 spoolman tests + 8 new = 589 green. Frontend build clean.
|
||||
|
||||
@@ -30,6 +30,10 @@ describe('getCurrencySymbol', () => {
|
||||
expect(getCurrencySymbol('UAH')).toBe('₴');
|
||||
});
|
||||
|
||||
it('returns BZ$ for BZD', () => {
|
||||
expect(getCurrencySymbol('BZD')).toBe('BZ$');
|
||||
});
|
||||
|
||||
it('returns the code itself for unknown currencies', () => {
|
||||
expect(getCurrencySymbol('XYZ')).toBe('XYZ');
|
||||
});
|
||||
@@ -49,7 +53,11 @@ describe('SUPPORTED_CURRENCIES', () => {
|
||||
expect(SUPPORTED_CURRENCIES.find((c) => c.code === 'MYR')).toBeDefined();
|
||||
});
|
||||
|
||||
it('has 28 entries', () => {
|
||||
expect(SUPPORTED_CURRENCIES).toHaveLength(29);
|
||||
it('contains BZD', () => {
|
||||
expect(SUPPORTED_CURRENCIES.find((c) => c.code === 'BZD')).toBeDefined();
|
||||
});
|
||||
|
||||
it('has 30 entries', () => {
|
||||
expect(SUPPORTED_CURRENCIES).toHaveLength(30);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const CURRENCY_SYMBOLS: Record<string, string> = {
|
||||
SGD: 'S$',
|
||||
NZD: 'NZ$',
|
||||
MXN: 'MX$',
|
||||
BZD: 'BZ$',
|
||||
MYR: 'RM',
|
||||
CZK: 'Kč',
|
||||
THB: '฿',
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
<!-- Splash screens for iOS -->
|
||||
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
|
||||
<script type="module" crossorigin src="/assets/index-CMHVQrcW.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BIzFPmFB.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-KYwGxnG9.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
Reference in New Issue
Block a user