diff --git a/CHANGELOG.md b/CHANGELOG.md index 22b3b2bf2..b5e0bcedb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -57,6 +57,7 @@ All notable changes to Bambuddy will be documented in this file. - **The frontend build no longer warns about `path` and `crypto` being externalized for the STEP previewer (#2976)** — `occt-import-js`, the Emscripten build behind STEP previews, requires both modules, but only inside its `ENVIRONMENT_IS_NODE` branches; in the browser it loads its `.wasm` from the URL the preview worker passes and draws randomness from `crypto.getRandomValues`. Vite still externalized both and printed two warnings on every build. `vite.config.ts` now drops exactly those two warnings for that one package through `build.rolldownOptions.onLog`, so an externalization anywhere else, or of any other module, still shows. ### Fixed +- **The Reorder Alert and Stock Break Alert notifications are now actually sent (#2955, reported and contributed by @ojimpo in #3196)** — Both events could be switched on for a notification provider, but nothing in the backend worked out when a SKU was low: the Forecast panel on the Inventory page did that in the browser, so no alert could ever fire. Bambuddy now runs the same forecast once an hour, from your own inventory or from Spoolman, and sends an alert when a SKU reaches its reorder point or will run out before a reorder could arrive. Each SKU alerts once when it enters a condition and again only after it has cleared; one that gets worse, from reorder to stock break, alerts again. A SKU with its alerts snoozed is skipped, and a provider with both events on gets the stock break message only. What has been sent is stored, so a restart or an update does not send the alerts again. The default templates now name the subtype and colour, so two colours of one filament no longer send the same message; a template you have edited is left as it is. - **Reading a 3MF's details no longer loads all of its geometry into memory** — To find the title, designer and MakerWorld link, Bambuddy read the whole `3D/3dmodel.model` entry into memory. A plain 3MF keeps its meshes in that same entry, so a large one cost hundreds of MB just to read a few metadata lines; a combined plate at the size limits pushed memory from 1.0 to 1.7 GB. The mesh data is now streamed past, and saving a 3MF to the library parses it without holding up other requests. - **The queue starts jobs in the order you put them, whether they're pinned to a printer or queued for "Any " (#3200, reported by @bgrr74)** — With a job pinned to a printer dragged above two "Any P2S" jobs, the printer finished and started one of the "Any" jobs from further down, while the pinned job sat at the top showing "Busy". The scheduler sorted the queue by target first and position second, so position only counted among jobs with the same target. Which job won a printer both wanted then came down to the database: on SQLite "Any" jobs always won, and on PostgreSQL pinned jobs did. The scheduler now follows the order the queue page shows, and so does Shortest Job First, whose starvation guard now also protects a pinned job that an "Any" job jumped, and the other way round. New jobs, batch copies, virtual printer and webhook uploads go to the end of the whole queue, not the end of their printer's part of it, and "add to top" puts them at the top of the whole queue. Likely also the cause of #1808. - **Settings tabs keep their icons when a label is long (#3191, reported and contributed by @Thomansky in #3192)** — In German the Notifications tab lost its bell: "Benachrichtigungen" plus the provider count didn't fit the tab list on wide screens, and the icon was squeezed to nothing instead of the word. The tab list is a little wider now, and tab icons, counts and status dots always keep their size, so a long label can only wrap. "Virtueller Drucker" also fits on one line again. @@ -176,7 +177,7 @@ All notable changes to Bambuddy will be documented in this file. - **A clear spool synced to Spoolman as pure black (#2912, reported and contributed by @ojimpo in #2924)** — The AMS reports a translucent roll as `00000000`, and every write to Spoolman truncated that to six characters before storing it, so a PETG Translucent spool arrived as opaque black and the external catalogue then named it "Black". Spoolman's own schema accepts eight characters, so the value Bambuddy was discarding was one the backend would have taken verbatim. #1545 fixed exactly this for the built-in inventory and left the Spoolman path behind, which is why internal mode has been storing the alpha correctly for months. The read side was the matching half: it rejected anything that was not exactly six characters, so fixing the writes alone would have turned clear spools grey instead of black. Eight characters are stored only when the alpha byte says the filament is genuinely translucent — passing everything through would rewrite the colour of every opaque spool on its next touch, churning records in people's Spoolman for no benefit. Colour comparisons now key on the shape a value would be stored as, so two colours match exactly when storing them would produce the same value. That is what keeps the widening safe in both directions: an opaque tray still finds the six-character filaments every existing instance is full of, so no upgrade mints a duplicate for every spool on the next sync, while a clear roll gets its own record instead of being conflated with the black one of the same RGB. The edit route compares the same way, so a no-op edit no longer PATCHes the filament on every save and an alpha-only edit still reaches Spoolman. One consequence is worth stating: a filament already stored wrongly-opaque by this bug gets a second, correct record the next time that roll is auto-added, rather than the old one silently capturing every clear spool that follows. - **Translucent spools showed as an empty circle or as solid black in four more places** — The swatch helper had two answers, the transparency checkerboard for a fully clear colour and a flat fill for everything else, so a half-translucent spool rendered identically to an opaque one. The AMS tray swatches never reached that helper at all: Assign Spool painted the reported colour directly, so a clear tray was an invisible circle, and Configure AMS Slot cut the alpha off first, so a clear tray was solid black — the same symptom as the sync bug above, in the UI, and present regardless of which inventory mode is in use. All of them now draw through one helper, which lays a partly translucent colour over the checkerboard so the swatch shows both the tint and that it is see-through. - **Every notification provider vanished from the list after the inventory toggles were wired up** — Adding `on_stock_reorder_alert` and `on_stock_break_alert` to the provider schema made them required on the way out as well as the way in, because the response model inherits the write model. Every `on_*` column on `notification_providers` is nullable with no server default, and on an install where the table had already been created from the ORM metadata before migrations ran, the `ALTER ... DEFAULT false` that introduced those two columns was swallowed as a duplicate and never backfilled the rows that were already there. Those NULLs sat harmless for as long as nothing read them; the moment the flags were declared on the response, the row failed validation, and since a list is validated as a whole, one such row took every provider down with it. The API returned a 500 and the UI rendered what it was given — an empty list — so correctly configured providers looked deleted while sitting untouched in the database. They are backfilled to off on the next start, matching what the sender already did with them: it selects providers with `IS TRUE`, so a NULL flag never sent anything. A NULL flag now also reads as off rather than failing the response, so the next flag added to that schema cannot repeat this. -- **Two inventory notification toggles could never be turned on, so stock alerts have never been able to fire (#2945, reported by @ojimpo; regression tests contributed by @ojimpo in #2956)** — `on_stock_reorder_alert` and `on_stock_break_alert` exist as columns on a notification provider, have their own templates, and `notification_service` looks providers up under exactly those names before sending. The whole UI is there too: a toggle in Add/Edit Notification, a badge on the provider card, the field in the API client's types, and tests for all of it. The one thing missing was the schema. `NotificationProviderCreate`/`Update` never declared either field, and Pydantic drops what it does not declare, so every request that carried them came back `200 OK` with the row unchanged — and `_provider_to_dict`, which is a hand-maintained field-by-field map, never returned them either, so the toggle read back off no matter what the database held. Nothing errored anywhere along that path. Both directions are wired now, and the round-trip tests that already covered the Home Assistant toggles cover these too, because the failure is structural rather than particular to one field: any column missing from those two maps is invisible to a test that builds providers through the ORM, and only a create-then-re-read through the route catches it. This makes the setting stick and report itself honestly; the detection side that would *call* those two senders does not exist yet, so turning them on does not yet produce notifications. The tests no longer depend on anyone remembering the next field, either. They now read the list of provider fields from the database model itself, so a toggle or setting added later is checked from the day its column exists, with no new test to write. It must be accepted when a provider is created and when it is edited, and every toggle must survive a save and read back from both the single-provider and the list endpoint. Each toggle is set to the opposite of its default for that check, because nine of them default to on and a test that saves "on" and reads back "on" would pass even if the value were silently dropped. +- **Two inventory notification toggles could never be turned on, so stock alerts have never been able to fire (#2945, reported by @ojimpo; regression tests contributed by @ojimpo in #2956)** — `on_stock_reorder_alert` and `on_stock_break_alert` exist as columns on a notification provider, have their own templates, and `notification_service` looks providers up under exactly those names before sending. The whole UI is there too: a toggle in Add/Edit Notification, a badge on the provider card, the field in the API client's types, and tests for all of it. The one thing missing was the schema. `NotificationProviderCreate`/`Update` never declared either field, and Pydantic drops what it does not declare, so every request that carried them came back `200 OK` with the row unchanged — and `_provider_to_dict`, which is a hand-maintained field-by-field map, never returned them either, so the toggle read back off no matter what the database held. Nothing errored anywhere along that path. Both directions are wired now, and the round-trip tests that already covered the Home Assistant toggles cover these too, because the failure is structural rather than particular to one field: any column missing from those two maps is invisible to a test that builds providers through the ORM, and only a create-then-re-read through the route catches it. This makes the setting stick and report itself honestly; what actually sends the alerts is the #2955 entry above. The tests no longer depend on anyone remembering the next field, either. They now read the list of provider fields from the database model itself, so a toggle or setting added later is checked from the day its column exists, with no new test to write. It must be accepted when a provider is created and when it is edited, and every toggle must survive a save and read back from both the single-provider and the list endpoint. Each toggle is set to the opposite of its default for that check, because nine of them default to on and a test that saves "on" and reads back "on" would pass even if the value were silently dropped. - **One Home Assistant sensor reporting a long text state could stop every printer sensor from updating** — `last_state` is a 64-character column, and the poller wrote whatever Home Assistant returned straight into it. A numeric entity that starts answering with free text - an enum, an error string from a template sensor - overflows that. SQLite stores it regardless, which is why this stayed quiet, but PostgreSQL rejects the row, and a poll pass commits every sensor at once: one such entity took the whole batch down on every tick, so no printer sensor's reading, timestamp or alert state advanced again, and the print interlock kept deciding against a frozen picture. What is persisted is now cut to the column, while the cached reading keeps the full state for display. The comparison that decides whether the state changed is made against the cut form too - comparing the stored value against the raw one would read as a difference on every single poll and churn `last_changed` forever. The storage-location poller was fixed the same way in the same release; both now go through one helper, each passing its own table's width. - **Configure Slot could bind the default K value for a profile the picker was visibly showing** — The AMS slot dialog sends `cali_idx` from `selectedKProfile`, which the mutation read through its own closure. React Query hands a mutation its options from an *effect*, so a click landing between a commit and that effect flushing runs the previous render's function - one that captured the selection as it was before the K-profile query resolved. The result is `cali_idx: -1`: the printer binds the default 0.020 rather than the calibrated K, while the dialog shows the right profile selected the whole time. It surfaced as an intermittent failure of the per-nozzle K-profile test, roughly one full-suite run in six, and reproducing it with staggered query resolution showed the divergence directly - the select element held the correct profile immediately before and after the click, and the payload still carried -1. That test's slot is the most exposed case in the file, a right-hotend slot carrying the left hotend's index, where the "keep showing the active profile" safety net cannot repair an empty recompute. The mutation now reads the selection from a ref written during render, so it resolves at execute time rather than at capture time; the same applies to the K value and the profile's ids, which travel in the same payload and had the same exposure. Measured over 27 runs of a staggered-resolution grid: 2 failures in 15 before, 0 in 12 after. The modal's printer-model query was also missing from the test file's mock, so it ran with no query function and rejected on every test in it - mocked now, though on its own that changed nothing, which is how the ref was confirmed as the fix rather than assumed. - **A print started from the printer's own screen swept every FTP path, archived blank, and then blamed a slicer setting (#1820, reported, captured and re-measured across four daily builds by @ojimpo)** — `current_project_url` was assigned in exactly one place, `_handle_request_message`, and that runs only for the request topic. A print started from the touchscreen publishes nothing there, so the field stayed empty for the one case the storage verdict exists for: the file is already in the printer's own model library under `/userdata/model/history/`, and port 990 does not serve it. The verdict then fell through to the `sdcard` flag — which @ojimpo's H2S reports as true, its "card" being the internal eMMC — so every such print ran the full sweep before giving up: 16 filename-and-directory attempts over 22 FTPS connections, 18 of them refused, 6.4 seconds, then a fallback archive with a name and nothing else. The printer does announce where the file lives. It arrives as an unsolicited `project_file` **response** on the report topic about two seconds before `gcode_state` reaches PREPARE, and Bambuddy now reads the `url` off it, gated on `result: SUCCESS` and a non-empty value so a refused dispatch cannot name a file that was never written. Reading it there rather than only at the request topic also covers an install nobody had in view: some brokers refuse the request-topic subscription, and on those no print of any kind had ever populated the field. Our own dispatch is echoed on both topics, so the new branch captures state and nothing else — the "external dispatch" diagnostic stays with the request-topic handler, which sees the echo first, and reusing it here would have logged every Bambuddy-started print as somebody else's. What the print names is now what gets tried: the five directories a copy could be in, rather than the ~110 connections that cannot succeed. That copy is worth trying, because an H2S keeps recently used jobs under `/cache` and archives them in full while they last; roughly eight files later the same job archives with a name only, which is exactly what @ojimpo measured on two prints the same day. Nothing about slicer-sent prints changed. **The banner that appears afterwards no longer describes a step that never happened.** With no reason recorded, a blank archive fell back to the original wording — "Store sent files on external storage" is off in your slicer, go and turn it on. On the reporter's printer that setting is on, and the internal-storage wording added in #2780 already explains why it would not help on an H2 anyway, so the archive that most needed that explanation was the only one that could not be given it. Prints started from the screen, from Handy, or by picking a file the printer already had now carry a reason of their own and their own wording: no slicer was involved, nothing was sent, and no setting changes it. What can be done instead is offered in its place — start the print from Bambuddy, or read the sliced weight off the printer's own file browser and enter it under **Filament used (g)** in Edit Archive, which the cost and the Projects totals then follow. Settings > Printers > Connection Diagnostic reads the same reason rather than a fixed one, so the two surfaces cannot end up giving the same printer different advice.