mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 12:46:49 +02:00
de8a86c40f26210df3faa68252b498edcfd46747
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ca93d4cf44 |
fix(backup): never restore a toggle whose credential can't come with it (#2656)
A settings restore refuses to write anything credential-shaped, but wrote the switches that depend on those credentials like any other key. Restoring the two halves apart is not a partial restore, it is a downgrade. The sharp case is Prometheus. /api/v1/metrics is on PUBLIC_API_ROUTES and its only gate is `if token:`, so an empty or absent token means no authentication at all. prometheus_token matches the `token` hint and is refused; prometheus_enabled is an ordinary key and was written. On an instance that never enabled Prometheus there is no local token row, so overwrite-*off* alone was enough to publish the whole metrics body to anyone who could reach the port. The new integration test shows exactly that: 200 with a full unauthenticated body before, 404 after. Four more pairs are the same shape and break an integration rather than open one: ldap_enabled/ldap_bind_password, mqtt_enabled/mqtt_password, ha_enabled/ha_token (with an HA_TOKEN env arm, since get_homeassistant_settings prefers the environment over the row), and virtual_printer_enabled/virtual_printer_access_code — the last largely vestigial post-migration, included for consistency. A toggle is refused only when all five hold: the payload value is truthy, the backup carried a non-empty companion credential, that credential is denylisted, this instance has no usable value for it, and the toggle is not already on locally. The second condition is what keeps the rule honest — an anonymous MQTT broker and an anonymous LDAP bind are legitimate configs that pass empty credentials straight through, and without it both would be false positives. With it, the rule fires only when the restore would produce a config weaker than both the backup and the local instance. A present-but-blank prometheus_token row counts as unusable, since that is precisely the `if token:` hole. The rule needs the payload *and* local database state, which the old static _count_items could not see, so preview and restore now share one classifier: _plan_settings() runs a single SELECT over both halves of every candidate pair before anything enters the session, and returns the three refusal buckets. preview() takes the session the route already has. _is_skipped_setting_key is gone rather than having its docstring corrected as asked: a name is no longer enough to decide, so the union predicate had no caller left. Also implements the review's third ruling — the tally counts what the preview counted, and refusals live in the notes. Two `skipped += 1` increments are dropped (blocked, protected) and the companion refusal adds none; the value-is- None and overwrite-off skips stay, because they depend on the run's flags, which the preview cannot see. restored + skipped + failed now equals the item count the user was shown — off by three before. Behaviour change called out for review: test_credential_keys_are_never_restored and test_auth_settings_are_never_restored asserted skipped == 2 and 4; both are now 0, which is the point of the ruling. 16 new unit tests plus 2 integration tests. Nine of them are controls, because over-refusal is the real risk of this change — the anonymous-broker and anonymous-bind guards are load-bearing, not decoration. |
||
|
|
07244b6a43 |
fix(backup): don't count a stale selection, and say when archive links are dropped (#2656)
Two smaller restore-path fixes from the same review. The modal's footer counted `selected` raw while the checkboxes rendered `selected && isAvailable`. Switching commits keeps `selected` on purpose (it is only pruned once the new preview lands), so for as long as the new commit's preview was in flight — with the category list replaced by its spinner — the footer still read "2 selected" over an enabled Restore button, and clicking it restored the newly-picked commit with the previous commit's categories, none of which the user had seen an item count for. The count and the POST body now come from one `selectedCategories` memo gated on availability, exactly as the checkboxes are, so both go empty until the preview lands. Restoring Spool inventory without Print archives leaves archive_id_map empty, so every usage -> archive link is nulled even where the archive exists locally. It can't be resolved here (the archives payload isn't fetched for a category that wasn't selected) and a later archives-only restore won't repair it either, since the usage dedupe key doesn't include archive_id and those rows read as already-present. So it gets a note naming the remedy while the user can still redo the run with both categories ticked. Carries the rebuilt bundle (index-C2LOlVCR.js -> index-C16HJNOV.js). |
||
|
|
e7495dd41b |
fix(backup): reconnect the MQTT relay after restoring mqtt_* settings (#2656)
The relay reads its broker config once, when configure() is called — which is
why the settings PUT handler reconfigures it after writing those rows
(api/routes/settings.py:246). The restore wrote the rows and stopped there, so
the relay stayed on the pre-restore broker until the next backend restart while
the UI showed the restored values: the one way a settings restore could look
applied without being applied.
_restore_settings now reports the keys it actually wrote, and run_restore
reconfigures the relay from the committed rows when any of them is an mqtt_ one.
Three details worth keeping:
* it runs after the commit, because configure() drops the connection and
rebuilds it — not something to do on values a later failure could roll back;
* it is keyed on written, not merely present: a key skipped for overwrite=off
or by the credential blocklist must not trigger a reconnect;
* mqtt_password is never restorable, so configure() gets the row already in the
database and an unchanged broker keeps working.
A broker that refuses the new config is noted on the settings tally ("restart
Bambuddy") rather than failing the restore, matching the PUT handler's
best-effort handling of the same call.
|
||
|
|
3ba89c60a1 |
fix(backup): release the SQLite writer before the K-profile MQTT phase (#2656)
_apply ran archives and spools first, which autoflushes their INSERTs and so opens SQLite's single write transaction, then called _restore_kprofiles — which awaits get_kprofiles per printer per nozzle at timeout=5.0 with max_retries=3, i.e. up to ~15 s each against a printer that ignores the request. The commit only came afterwards, in run_restore. busy_timeout is 15 s (core/database.py:21), so a restore covering a couple of unresponsive printers held the writer past it and unrelated writes elsewhere in the app failed with "database is locked". Commit the database categories before the MQTT phase starts. The K-profile work is not in that transaction anyway — it leaves over MQTT — so the only thing lost is rolling those categories back when a K-profile send fails, and that rollback was never the right behaviour: extrusion_cali_set has already reached the printer by then, so undoing the database half would just make the two disagree. |
||
|
|
737258202b |
fix(backup): never restore the auth policy settings from a backup (#2656)
_collect_settings exports every Settings row minus two credential keys, so auth_enabled / advanced_auth_enabled / local_login_enabled / setup_completed all travel in a backup, and none of them are credential-shaped enough for _SECRET_KEY_HINTS to catch. Writing them back was the one part of a settings restore that changed who can reach the instance rather than how it behaves: * auth_enabled=false — from any backup taken before auth was turned on — disabled authentication. core.auth caches only the enabled=True result, on a 30 s TTL, precisely so staleness fails closed; set_auth_enabled pairs its write with invalidate_auth_enabled_cache(). The restore did neither, so it left the stored value the open one. * local_login_enabled=false walked straight past the #1589 refusals in update_settings (no enabled OIDC provider / no OIDC link on the caller), which exist to stop exactly that lockout. * /github-backup/restore is gated on GITHUB_RESTORE alone, so honouring these keys made that permission a way to rewrite auth config without SETTINGS_UPDATE. Flipping an existing row needed overwrite_existing, so the odds were lower than the severity. Both refusals now share _is_skipped_setting_key so the preview's item count still matches what a restore writes, and the skipped keys get their own note pointing at the auth UI rather than being folded in with the credential ones. |
||
|
|
cbe412607f |
fix(backup): resolve K-profile cali_idx live instead of reusing the backup's (#2656)
Restoring K-profiles addressed extrusion_cali_set at the cali_idx recorded in the backup. If that slot no longer existed on the printer the write was silently dropped and the restore still reported the profile restored. Not an edge case: Bambuddy's own K-profile editor is what re-keys the slot. On a single-nozzle printer an edit is delete-then-add, so any edit between backup and restore reproduces it. Found testing on an X1E. Backup held cali_idx 8151; an edit through the UI re-keyed the profile to 4606; the restore published cali_idx 8151, the printer ignored it, and the tally read "1 restored" while the k-value stayed put. Resending the identical payload with cali_idx 4606 applied, isolating the stale index as the cause. Fix mirrors the natural-key matching spools and archives already use, which the module docstring already promised but scoped to spool.id and print_archives.id. Before writing, read the live profiles for the nozzle and match on filament_id + setting_id, falling back to filament_id + name, then to the sole candidate for that filament. Send that profile's current cali_idx; where nothing matches send -1 so the printer adds a new profile instead of addressing a dead slot, and say so in the tally. A failed read degrades to adding rather than aborting. Also corrects the tally note. The printer does acknowledge extrusion_cali_set -- it answers with a result/reason pair -- so "published without acknowledgement" was false. It reports "fail" on writes that land, though, so the note now says the acknowledgement is unreliable rather than absent. Consuming result is left to a follow-up. Re-verified on the same X1E: perturbed to k=0.061, restored from the commit carrying the stale slot, payload went out with cali_idx 4606 and the printer read back 0.027. |
||
|
|
6a239314dc |
feat(backup): restore selected categories from a Git backup commit (#2656)
The Git backup feature was push-only: there was no equivalent of the local backup's Restore button, so recovering meant hand-downloading JSON files from the repository. This adds the read side. Providers gain list_commits / list_tree / fetch_files on the GitProviderBackend ABC. GitHub implements them against the Git Data API and Gitea/Forgejo inherit that unchanged; GitLab overrides for its own REST shape, including tree pagination and subgroup path encoding. fetch_files is batched so the path -> blob SHA lookup happens once per restore rather than once per file, and uses the blobs API rather than contents because contents silently inlines only the first 1 MB. The new GitHubRestoreService resolves HEAD to a concrete SHA up front, so a preview and the restore that follows act on the same commit even if a scheduled backup lands in between. Categories are applied archives -> spools -> settings -> kprofiles: archives first because spool usage history references archive_id, K-profiles last because they leave the database and publish over MQTT. Restores never reuse the backup's primary keys. spool.id and print_archives.id are bare autoincrement columns, so ids from an old backup very likely belong to unrelated rows today; rows are matched on natural keys (tag_uid, then tray_uuid, then a descriptive composite for spools; content_hash or filename plus started_at for archives), inserted without an explicit id, and an old_id -> new_id map rewrites the foreign keys in spool usage history. created_at is carried across on insert so restoring the same backup twice matches instead of duplicating. Dangling printer/project links are cleared and reported rather than failing the row. Settings restore re-applies the collector's credential denylist on the read side, plus a pattern guard, because a backup taken before that denylist existed can still contain secrets. Restored archives are metadata-only: the 3MF and thumbnail bytes are not in a Git backup and print_archives.file_path is NOT NULL, so inserted rows get an empty path and the UI says so. Backup and restore take a mutex against each other; both write the same tables and talk to the same printers. Restores are logged as GitHubBackupLog rows with trigger="restore", which needs no migration and surfaces them in the existing History card. Cloud profiles are deliberately not a restore category. The collector never actually writes cloud_profiles/*.json - it reads a "setting" list key the Bambu Cloud API does not return - and the preset list it would write carries no setting payload. Filed separately. Permission github:restore already existed and is granted to Administrators, so no permission changes were needed. Tests: 125 new backend tests (provider reads across all four providers, the per-category appliers, the API endpoints) and 13 frontend tests. Full suites pass with no regressions; the 35 backend failures on Windows are byte-identical with and without this branch. |