mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
fix(printer-models): swap A1 / A1 Mini in PRINTER_MODEL_ID_MAP
N1 and N2S were flipped relative to every other registry that names them - firmware_check.py (N2S -> "a1"), virtual_printer/manager.py (both the model map and the serial-prefix map: N2S -> 039 = A1, N1 -> 030 = A1 Mini), and printer_manager.py A1_MODELS all agree on N2S = A1, N1 = A1 Mini. Only printer_models.py had it backwards, so any path that resolved an A1-family printer by internal code rather than serial prefix would silently misclassify. Also fixes the matching comments in LINEAR_RAIL_MODELS - cosmetic only (both codes were already in the frozenset) but kept the file self-consistent. New TestA1SeriesModelIds regression test pins both directions so a future re-flip fails loudly.
This commit is contained in:
@@ -18,6 +18,8 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- **Print Log page: per-row delete (#1687 part 1, reported by @IndividualGhost1905)** — Reporter noted that the existing "Also remove this print from Quick Stats" toggle on archive delete is one-shot: if you tick "keep stats" at delete time, there was no later way to drop the row from /stats; and rows that aren't tied to an archive (errors, aborts, manual entries) had no delete affordance at all. **Fix:** every row in the Archives → Print Log table now has a trash icon next to the filament cell, gated on `archives:delete_own` (own rows) or `archives:delete_all` (any row), matching the archive-delete permission shape. Click → confirm modal → row is gone, and because /archives/stats aggregates over `PrintLogEntry` the filament / time / cost contribution drops out of Quick Stats in the same response cycle. The matching archive (if any) is untouched — the log row is a sibling, not a child. **Backend:** new `DELETE /print-log/{entry_id}` mirrors `delete_archive`'s ownership flow via `require_ownership_permission(ARCHIVES_DELETE_ALL, ARCHIVES_DELETE_OWN)`; owners can drop their own rows, admins can drop any row, missing IDs return 404 rather than 200-silently. **Frontend:** new `deletePrintLogEntry` API helper, per-row mutation that invalidates both `print-log` and `archives-stats` query keys so the totals re-render without a manual refresh. **i18n:** 4 new keys (`deleteEntryTitle`, `deleteEntryConfirm`, `entryDeleted`, `entryDeleteFailed`) translated across all 11 locales (de / en / es / fr / it / ja / ko / pt-BR / tr / zh-CN / zh-TW). **Tests:** 3 backend integration cases — delete drops the row from /stats while keeping the linked archive listed, missing ID returns 404, delete-one does not touch siblings (regression guard against an accidental `delete(PrintLogEntry)` without a `where`). Frontend ArchivesPage / PrintLogModal vitests stay green (31 / 31). i18n parity green (5099 leaves × 11 locales). Issue #1687 also asks for per-row tagging (already covered by `EditArchiveModal`'s tags field) and per-row filament-usage-history edits (deferred — see the issue thread for the reasoning).
|
||||
|
||||
### Fixed
|
||||
- **A1 / A1 Mini internal-code map was swapped in `PRINTER_MODEL_ID_MAP` (surfaced while scoping A2L support, #1684)** — `backend/app/utils/printer_models.py` mapped `N1 → "A1"` and `N2S → "A1 Mini"`, but every other registry that names these codes — `firmware_check.py` (`N2S → "a1"`), `virtual_printer/manager.py` (both the model map and the serial-prefix map: `N2S → "03900A"` is the A1's `039` prefix, `N1 → "03000A"` is the A1 Mini's `030`), `printer_manager.py` `A1_MODELS` — consistently uses the opposite (correct) direction. Any path that resolved an A1-family printer by internal code rather than serial prefix would silently misclassify. **Fix:** swap `PRINTER_MODEL_ID_MAP` to `N1 → "A1 Mini"`, `N2S → "A1"`; the matching comment in `LINEAR_RAIL_MODELS` was also wrong and got the same swap (the frozenset's contents don't change — both codes were already in it — so this is cosmetic, but kept the file self-consistent). New regression test class `TestA1SeriesModelIds` pins both directions so a future re-flip fails loudly. Functional impact in practice is small (most A1 detection runs off the serial prefix), but the inconsistency was a footgun for any future caller that trusted `normalize_printer_model_id`. Backend printer-model suite 46 / 46 green; ruff clean.
|
||||
|
||||
- **PostgreSQL restore from a SQLite backup no longer deadlocks against the print scheduler (reproduced 2026-06-09 restoring a native install's backup into a fresh Docker+Postgres deploy)** — Reporter (Maziggy) backed up the native install, brought up the new Docker image against an external Postgres, hit Restore in Settings → Backup. ~2 seconds in, the restore aborted with `asyncpg.exceptions.DeadlockDetectedError: Process X waits for AccessExclusiveLock on relation 109940; Process Y waits for RowExclusiveLock on relation 110182`. **Root cause: the existing `close_all_connections()` step before the DB swap only disposes the SQLAlchemy engine's connection POOL — the asyncio tasks that USE the engine keep running.** The `print_scheduler.run()` loop (30 s cadence) and `smart_plug_manager._snapshot_loop()` (30 s cadence) wake up after the dispose, call `async_session()`, lazily reopen a pool connection, and start a normal transaction that grabs `RowExclusiveLock` on `print_queue` / `smart_plug_energy_snapshots`. The restore's `DROP TABLE IF EXISTS public.<tbl> CASCADE` pass in `_import_sqlite_to_postgres` needs `AccessExclusiveLock` on every public table — AB/BA lock-order conflict, classic Postgres deadlock, restore transaction rolled back. The log confirms: `13:44:53,669` restore begins → `13:44:53,680` print_scheduler fires queue check → `13:44:55,607` smart_plug_manager fires snapshot → `13:44:55,607` deadlock detected. The existing code already paused `virtual_printer_manager` before restore for file-lock reasons; the other timer-based DB writers were missed. **Fix — two layers.** (1) Before `close_all_connections()`, pause the four most active timer-based DB writers via their existing stop affordances: `print_scheduler.stop()`, `smart_plug_manager.stop_scheduler()`, `notification_service.stop_digest_scheduler()`, `await background_dispatch.stop()`. Then `await asyncio.sleep(1.0)` to let in-flight loop iterations commit and release their sessions before the engine pool gets disposed. We don't restart the services on success because the restore handler already tells the user to restart Bambuddy to pick up the new DB. (2) Belt-and-braces inside `_import_sqlite_to_postgres`: prepend `SET LOCAL lock_timeout = '10s'` to the begin-block before the `DROP TABLE CASCADE` pass, so any residual writer that slips through the pause window (per-printer MQTT clients writing reactively to state changes, the hourly AMS history recorder firing inside the restore window, etc.) surfaces a fast `lock_timeout` error instead of producing a fresh deadlock or hanging the restore for 30+ seconds. `SET LOCAL` is transaction-scoped so the global default applies to every other DB caller. Scope clarification: there are ~12 background services started at lifespan startup; the four paused here are the ones with the tightest cadences. Slower-cadence services (`github_backup_service`, `local_backup_service`, `library_trash_service`, `archive_purge_service`, AMS history, runtime tracking, SpoolBuddy watchdog, camera cleanup) all fire on hour-or-longer intervals and are statistically very unlikely to land inside a few-second restore window; the lock_timeout layer catches them if they do. **Tests**: `test_restore_sqlite_wal_safety.py` and `test_settings_api.py` integration suites (53 tests) stay green on the edited handler; ruff clean; runtime smoke (`from backend.app.services.X import Y` + `hasattr` + `iscoroutinefunction` check) confirms all four stop signatures match the patch's sync/async mix.
|
||||
|
||||
- **Configure Slot now keeps the active K-profile on reopen for assigned-but-unconfigured slots (#1689 follow-up, reported and patched by @Spionkiller01)** — After the original #1689 fix shipped, Spionkiller01 found a residual case: on a slot that's *physically loaded but unconfigured* (filament inserted, but the printer hasn't bound a preset yet — `tray_type=""`, `tray_info_idx=""`, no `slot_preset_mappings` row), the first open of Configure Slot showed the right K-profile, but closing it with the X and reopening it dropped back to "default 0.020". Clicking "Configure slot" (Apply) once persisted it, but the user shouldn't have to. **Root cause: the original #1689 cali_idx safety net was unreachable on this code path.** `matchingKProfiles` in `ConfigureAmsSlotModal.tsx:751` early-returned `[]` when `selectedPresetInfo` was null — and `selectedPresetInfo` resolves to null exactly when there's no resolvable slot preset (unconfigured slot, no mapping row). The "always include the slot's currently-active K-profile by cali_idx" branch lives *past* the main name+id matcher, so it never ran from the no-preset path. On first open a freshly-cached preset briefly let the safety net trigger; on reopen the live slot state had no preset, returned `[]`, the auto-select effect saw no candidates, the modal fell back to default 0.020. **Fix (verbatim from Spionkiller01's H2C-tested diff, with the existing extruder guard):** split the early return into two — still short-circuit on missing kprofilesData, but when `selectedPresetInfo` is null and `slotInfo.caliIdx > 0`, find the active profile by `slot_id === activeIdx` (extruder-matched when known) and return it as a single-item list. The auto-select effect downstream then pre-selects it on reopen with no extra change. Strictly additive: with a resolvable preset present the existing matcher runs untouched; with `caliIdx === 0 || null` the function still returns `[]` (no unrelated profiles leak in). **Tests:** new vitest case `surfaces the slot's active K-profile when no preset is resolvable (#1689 follow-up)` exercises the path with `trayType=''`, no `savedPresetId`, and `caliIdx=6` against a K-profile fixture at `slot_id=6` — asserts the dropdown surfaces it. Verified the test fails without the patch (stash → run filter → fail; pop → run → pass). The existing `caliIdx === 0` guard test continues to pass under the new branch. Full ConfigureAmsSlotModal vitest 24/24 green. **Credit:** @Spionkiller01 for spotting the residual edge case after merge, producing the diff, and testing live on an H2C — `Co-Authored-By` on the commit.
|
||||
|
||||
@@ -42,8 +42,8 @@ PRINTER_MODEL_ID_MAP = {
|
||||
# A1 series
|
||||
"A11": "A1",
|
||||
"A12": "A1 Mini",
|
||||
"N1": "A1",
|
||||
"N2S": "A1 Mini",
|
||||
"N1": "A1 Mini",
|
||||
"N2S": "A1",
|
||||
"A04": "A1 Mini",
|
||||
# H2 series (Office/H series)
|
||||
"O1D": "H2D",
|
||||
@@ -96,8 +96,8 @@ LINEAR_RAIL_MODELS = frozenset(
|
||||
"H2C",
|
||||
"H2S",
|
||||
# Internal codes
|
||||
"N1", # A1
|
||||
"N2S", # A1 Mini
|
||||
"N1", # A1 Mini
|
||||
"N2S", # A1
|
||||
"A04", # A1 Mini (alternate)
|
||||
"A11", # A1
|
||||
"A12", # A1 Mini
|
||||
|
||||
@@ -107,6 +107,22 @@ class TestX2DModel:
|
||||
assert "N6" in STEEL_ROD_MODELS
|
||||
|
||||
|
||||
class TestA1SeriesModelIds:
|
||||
"""Regression guard for the A1-family internal-code → display-name map.
|
||||
|
||||
The serial-prefix and firmware-API key tables across the codebase agree
|
||||
that N2S is the A1 (serial prefix 039) and N1 is the A1 Mini (serial
|
||||
prefix 030). PRINTER_MODEL_ID_MAP had these swapped, which silently
|
||||
misclassified A1 as A1 Mini in any path that resolved by internal code.
|
||||
"""
|
||||
|
||||
def test_n2s_is_a1(self):
|
||||
assert normalize_printer_model_id("N2S") == "A1"
|
||||
|
||||
def test_n1_is_a1_mini(self):
|
||||
assert normalize_printer_model_id("N1") == "A1 Mini"
|
||||
|
||||
|
||||
class TestDualNozzleModel:
|
||||
"""is_dual_nozzle_model — the single source of truth for nozzle class,
|
||||
consumed by start_print, the K-profile routes, and the re-slice guard."""
|
||||
|
||||
Reference in New Issue
Block a user