|
@@ -19,6 +19,7 @@ All notable changes to Bambuddy will be documented in this file.
|
|
|
- **The Windows installer build is split in two so a signing request can wait for a human (SignPath Foundation)** — Release tags are Authenticode-signed through the SignPath Foundation OSS programme, and the production certificate does not sign on demand the way the self-signed test certificate does: every request has to be approved by hand in the SignPath UI, because the Foundation verifies what is being signed and which build it came from. The submitting action waits for that approval with a default timeout of 600 seconds, which is ample when the test policy approves automatically in seconds and far too short once the wait is a person noticing a tag went out. A tag pushed at night would have failed the run ten minutes later with the installer already compiled and thrown away. The compile now ends in its own job that uploads the unsigned artifact and stops; a second job downloads it, signs it, and does the release-facing work, with the wait raised to an hour. Because the artifact is uploaded before the wait begins and is addressed by id, a missed approval window is recovered by re-running the second job alone rather than rebuilding the installer — which is the reason to separate them rather than simply raise the timeout in place. The second job runs for unsigned builds too, so the daily prereleases that are deliberately left unsigned to preserve the signing quota keep going out through exactly one set of alias, artifact and release steps. The property that matters is unchanged and now recorded next to the steps that depend on it: none of the alias, upload or release-attach steps carry `always()`, so GitHub skips all three when signing fails or times out, and an unsigned `.exe` cannot reach a release. Nothing about the signed output changes, and the restructure behaves identically under the test policy — the request simply completes immediately instead of waiting — so it can be proven green before the production certificate arrives.
|
|
- **The Windows installer build is split in two so a signing request can wait for a human (SignPath Foundation)** — Release tags are Authenticode-signed through the SignPath Foundation OSS programme, and the production certificate does not sign on demand the way the self-signed test certificate does: every request has to be approved by hand in the SignPath UI, because the Foundation verifies what is being signed and which build it came from. The submitting action waits for that approval with a default timeout of 600 seconds, which is ample when the test policy approves automatically in seconds and far too short once the wait is a person noticing a tag went out. A tag pushed at night would have failed the run ten minutes later with the installer already compiled and thrown away. The compile now ends in its own job that uploads the unsigned artifact and stops; a second job downloads it, signs it, and does the release-facing work, with the wait raised to an hour. Because the artifact is uploaded before the wait begins and is addressed by id, a missed approval window is recovered by re-running the second job alone rather than rebuilding the installer — which is the reason to separate them rather than simply raise the timeout in place. The second job runs for unsigned builds too, so the daily prereleases that are deliberately left unsigned to preserve the signing quota keep going out through exactly one set of alias, artifact and release steps. The property that matters is unchanged and now recorded next to the steps that depend on it: none of the alias, upload or release-attach steps carry `always()`, so GitHub skips all three when signing fails or times out, and an unsigned `.exe` cannot reach a release. Nothing about the signed output changes, and the restructure behaves identically under the test policy — the request simply completes immediately instead of waiting — so it can be proven green before the production certificate arrives.
|
|
|
|
|
|
|
|
### Fixed
|
|
### Fixed
|
|
|
|
|
+- **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** — `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.
|
|
- **Two inventory notification toggles could never be turned on, so stock alerts have never been able to fire** — `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.
|
|
|
- **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.
|
|
- **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.
|
|
- **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.
|