diff --git a/CHANGELOG.md b/CHANGELOG.md index 30e28715a..463a0c9d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ All notable changes to Bambuddy will be documented in this file. ### Added - **Preheat & Heat Soak before queued prints — per-item override + per-filament chamber targets (#1468, reporter @embed-3d)** — New scheduler stage that heats the bed (and the chamber, on printers that support it) and holds at temperature before each queued print starts, intended for engineering filaments (PA, ABS) where adhesion and warp depend on a warm chamber. **Why it doesn't already work in the slicer.** BambuStudio / OrcaSlicer can emit `M191` (wait-for-chamber-temp) in start-G-code, but Bambu firmware silently ignores `M191`, so any "wait for chamber" line in the slicer's start sequence is a no-op. The reporter confirmed this by trying the [MakerWorld chamber-heating G-code](https://makerworld.com/de/models/1200262-g-code-for-x1c-chamber-heating-and-heat-soak) in OrcaSlicer and finding the chamber-heating step wouldn't fire. Implementing this at the orchestration layer — Bambuddy waiting on `state.temperatures` between FTP upload and `start_print` — is the right architectural place; the slicer side is a dead end. **Where it fires.** `print_scheduler._preheat_and_soak()`, called from `_start_print()` immediately before the FTP upload section (`print_scheduler.py:2306`-ish). Best-effort: any failure (printer drops, gcode refused, no bed temp in metadata) logs and returns rather than failing the queue item — the normal upload + start path runs straight after. **Hardware-tier behaviour (three branches; the OP collapsed two of them and we kept them distinct):** (1) Active chamber heater — `H2C / H2D / H2D Pro / H2S / X2D / X1E` (`supports_chamber_heater()` true) — dispatches `M141 Sx` for the configured target then polls `state.temperatures["chamber"]` against it. (2) Chamber sensor only — `X1C / P2S` (`supports_chamber_temp()` true but `supports_chamber_heater()` false) — no `M141`, polls the chamber sensor and considers the chamber phase satisfied when bed radiation has driven the sensor to target. Radiant warm-up to ABS-friendly temps on a cold X1C is 20-30 min — the `max_wait_seconds` cap (default 900 s, range 60-3600) is a hard ceiling so a cold room can't stall the queue indefinitely; falls through to the soak phase if the chamber never converges. (3) No chamber sensor — `P1S / P1P / A1 / A1 Mini` — no chamber wait possible (the `chamber_temper` value these models report is meaningless per `printer_manager.supports_chamber_temp`), so only the bed phase + soak timer apply. **Bed target** is read from the archive's parsed `bed_temperature` metadata (the same `bed_temperature_initial_layer` / `bed_temperature` field `archive.py:438` already extracts from the 3MF); if missing the preheat stage skips and logs rather than guessing a default that might wreck a non-PLA print. **Settings — Settings → Workflow → Queue & Dispatch → Preheat & Heat Soak card.** Master toggle (`preheat_enabled`, default off — disabled installs see no behavioural change), `preheat_chamber_target` (°C, 0-60, default 0 = chamber phase disabled; PA: 50, ABS: 45, PETG-CF: 40), `preheat_max_wait_seconds` (60-3600, default 900), `preheat_soak_seconds` (0-1800, default 300). The numeric fields auto-disable in the UI when the master toggle is off so they read as "config that's not currently doing anything." A static helper line under the inputs spells out the three hardware tiers so users don't have to consult the wiki to know what their printer will do. **Tests.** Eight new cases in `backend/tests/unit/test_scheduler_preheat.py`: disabled-setting skip (no M140 / M141 dispatched), no-bed-temp-in-archive skip, `H2D` dispatches both `M140` and `M141`, `X1C` dispatches only `M140` (the explicit chamber-sensor-but-no-heater regression guard — wiring this to `supports_chamber_temp()` alone would have falsely fired `M141` on the entire X1 family), `P1S` ignores its meaningless `chamber` reading and lets only the soak timer run, `preheat_chamber_target=0` keeps the bed phase but skips chamber even on a heater-capable printer, lost-client mid-flow returns silently, lost-state mid-wait exits the poll loop gracefully and still soaks. `asyncio.sleep` patched to `AsyncMock` so the soak phase doesn't actually wait — assertions are on what was scheduled, not wall-clock. 8/8 green. **Wiki.** `bambuddy-wiki/docs/features/monitoring.md` gains a new "Preheat & Heat Soak" subsection under the queue/scheduling area documenting the three hardware tiers and the four-setting interface, so users with X1C or P1S know upfront what the feature can and cannot do for their printer. **i18n.** 13 new keys × 11 locales (en/de/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW), real translations everywhere — no English fallback. **Scope.** Backend (scheduler + schema + settings route) + frontend (settings card + AppSettings type) + tests + wiki. No DB migration (uses the existing key/value `Settings` table). No new permission (Settings → Workflow already gates on the same admin scope). The OP's "select option per print" UX is not part of this drop — preheat is a global default applied to every queued item that has a parseable bed temperature; per-queue-item override would need a `PrintQueueItem` schema migration plus print-modal and queue-modify UI work that would have widened the change beyond the scope agreed with the user. **Rework on user review.** First cut shipped a global-only design: one single `preheat_chamber_target` int in Settings → Workflow, no per-print override. User flagged two gaps on review: (1) you can't enable preheat for a single queue item, (2) different filaments need different chamber temps but the setting was a single value. Both are fair: PA needs 50, PETG-CF wants 40, PLA wants 0, but the global single-int forced one number across all of them. Reworked the data shape: replaced the single `preheat_chamber_target` int with `preheat_filament_targets` (JSON map of normalised filament type → °C, user-editable in the same card via the new `PreheatFilamentTargetsEditor` component) and added two columns to `PrintQueueItem` — `preheat_override` (`inherit` / `on` / `off`, default `inherit`) and `preheat_chamber_target_override` (nullable int, beats the filament-map derivation). The scheduler's resolution order: `item.preheat_override == 'off'` skips entirely; `'inherit'` falls back to the global `preheat_enabled` master toggle; `'on'` forces the stage even when the global is off. Chamber target: `item.preheat_chamber_target_override` (explicit °C) > max of `preheat_filament_targets[normalize(t.tray_type)]` across loaded AMS slots > 0. Mixed PA+PLA load picks PA's 50 (max-across-slots, NOT lowest-common-denominator — PA's chamber requirement is the binding constraint, PLA doesn't suffer from being warm). PLA-only prints derive 0 and skip the chamber phase automatically without the user touching anything. The per-print UI lives in `PrintModal`'s "Print Options" panel — tri-state segmented control (Inherit / On / Off) plus an optional chamber-target override input (shown only when override ≠ Off, blank = use filament map). Same control in edit-queue-item mode so you can flip preheat on an already-queued print. Tests grew from 8 cases to 15 across three categories — override resolution (3), chamber-target derivation (5), hardware-tier branching (5), plus 2 helper-fn tests — all green. DB migration added: `preheat_override VARCHAR(10) DEFAULT 'inherit'` and `preheat_chamber_target_override INTEGER NULL` on `print_queue`, idempotent via `_safe_execute`. Existing rows behave exactly as before (inherit + null = use global). i18n grew from 13 keys to 19 × 11 locales — real translations for the new override radio + per-filament editor strings, no English fallback. Frontend `PrintQueueItem` TS interface and `PrintQueueItemCreate` / `PrintQueueItemUpdate` shapes updated to carry the new fields end-to-end. **PrintOptions.tsx now uses `options[key as 'bed_levelling']` for the boolean rows since `options[key]` would type-error against the new non-boolean preheat keys** — minor TS-only adjustment, no behavioural change. **Airduct flap follows the resolved chamber target — bidirectional, idempotent.** Reported by user mid-test on H2D: preheat ran M141 for ABS but the cooling/heating airduct flap stayed in cooling, the open exhaust vent actively fought the heater and the chamber crawled toward target. The H-series (H2C/H2D/H2D Pro/H2S), X2D, and P2S all have a motorised flap with two modes: cooling (modeId=0, open exhaust, vents heat — right for PLA/PETG/TPU) and heating (modeId=1, closed exhaust, recirculates warm air — right for ABS/ASA/PC/PA). Bambu's firmware does **not** auto-switch the flap based on M141; whatever mode the user last left it in persists. So a PLA→ABS workflow inherits PLA's cooling-mode flap and the heater never wins; conversely an ABS→PLA workflow inherits ABS's heating-mode recirculation and runs PLA's chamber hot. **Fix.** New `supports_airduct(model)` helper in `printer_manager.py` mirrors the frontend whitelist (`P2S`, `X2D`, `H2C`, `H2D`, `H2D Pro`, `H2S` plus their internal codes). In `_preheat_and_soak`, after the bed dispatch and BEFORE the chamber M141: read the printer's current `state.airduct_mode`, derive the desired mode from the resolved chamber target (`chamber_target > 0` → heating, `chamber_target == 0` → cooling), and only fire `set_airduct_mode` when current ≠ desired. **The bidirectional switch is the load-bearing part** per Martin: when the resolved chamber target is 0 (PLA-only print, or per-item override disables chamber), the flap MUST switch to cooling even on a heater-capable printer that was previously running ABS, otherwise the closed-flap recirculation cooks PLA. The idempotency check (read `state.airduct_mode`, compare against desired before sending) keeps the flap motor from cycling needlessly when it's already where we want it. **Gating** is on `supports_airduct(model)` only — distinct from `supports_chamber_heater(model)`: X1E has a chamber heater but no flap (skipped), P2S has a flap but no active heater (still flipped, because even a passive-sensor printer benefits from the right airflow for its filament); the intersection that needs both is H2C/H2D/H2D Pro/H2S/X2D and they all work. **No post-print restoration** per Martin's preference — once a preheat sets the flap, it stays there for the print's duration (the print itself wants the same mode the preheat picked) and for any subsequent prints until the next preheat decision flips it. Best-effort: any `set_airduct_mode` failure logs and continues; M141 still fires regardless so a stuck flap doesn't kill the print. **Tests.** Four new cases in `test_scheduler_preheat.py`: `test_h2d_chamber_heat_switches_airduct_to_heating` (the OP scenario — cooling→heating before M141 for ABS), `test_h2d_chamber_zero_switches_airduct_to_cooling` (heating→cooling for a PLA print on a previously-warm flap), `test_h2d_airduct_already_correct_idempotent` (no command sent when current mode matches desired), `test_x1c_no_airduct_flap_never_fires_set_airduct` (gate regression guard — X1C has chamber sensor + chamber heater logic adjacent but no flap, must not leak the command). 21/21 in the file now. +### Added +- **API keys can log maintenance — new "Manage Maintenance" scope for HA-style automations (#1832 follow-up, reporter @MorganMLGman)** — Follow-up on the closed #1832 thread: reporter noted that maintenance had no checkbox in the API-key permissions UI and wasn't clearly denied in the wiki, unlike the other admin-only surfaces. Root cause was that `MAINTENANCE_CREATE / _UPDATE / _DELETE` sat on `_APIKEY_DENIED_PERMISSIONS` in `backend/app/core/auth.py`, so every API-key call to `POST /maintenance/items/{id}/perform` (mark maintenance as done — the useful endpoint for a "cleaned nozzle every N hours" Home Assistant automation) returned `403 "API keys cannot be used for administrative operations."` **Fix.** New `can_manage_maintenance` scope flag on `api_keys`, following the same shape as `can_manage_library` and `can_manage_inventory`. Wires the three maintenance write permissions into `_APIKEY_SCOPE_BY_PERMISSION` under the new flag; drops them from the denylist. Covers the per-printer maintenance CRUD (assign/remove items, edit intervals, log completion), the type-catalog CRUD (system types + custom types), and — via `MAINTENANCE_UPDATE` on the perform endpoint — the load-bearing "reset counter" call. `MAINTENANCE_READ` stays under `can_read_status`. **Backfill choice.** Distinct from the `can_manage_library` / `can_manage_inventory` migrations, which mirrored `can_queue` to preserve existing capability from the prior denylist-model. Maintenance writes were EXPLICITLY denied for every API key pre-migration, so no existing integration relies on them — backfilling to `can_queue` would silently widen scope on upgrade for every queue-capable key without unlocking any real use case. Existing rows backfill to **FALSE**; new keys created via the Settings UI default to **TRUE** (matches the safe-on-by-default pattern for the two prior scopes). Users opt in per key from Settings → API Keys → Edit. **CLI.** Bundled SpoolBuddy kiosk key gets `can_manage_maintenance=False` — kiosks don't need it, keep them minimally scoped. **Tests.** RBAC matrix in `backend/tests/integration/test_auth_apikey_rbac.py` extended: `valid_flags` set + `_each_scope_flag_has_at_least_one_permission` parametrize decorator + `_FakeApiKey.__init__` all pick up the new flag; three new `_SCOPE_CASES` for `MAINTENANCE_CREATE / _UPDATE / _DELETE`; the cross-scope leakage guard (`other_flags` set at line 355) widened from four flags to six so a `can_manage_library` toggle can't accidentally allow a `MAINTENANCE_UPDATE` call (and vice versa). 78/78 in `test_auth_apikey_rbac.py` + adjacent auth suites green. **Frontend.** New checkbox in the Settings → API Keys create form (between Manage Inventory and Allow Cloud Access), new teal badge in the key list, TS types extended (`APIKey`, `APIKeyCreate`, `APIKeyUpdate`). **i18n.** Three new keys (`settings.manageMaintenance`, `settings.manageMaintenanceDescription`, `settings.maintenanceBadge`) × 11 locales (de/en/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW), real translations everywhere per convention. **Wiki.** `bambuddy-wiki/docs/features/api-keys.md` — new row in the permissions table, new bullet in the Principle-of-Least-Privilege list ("Home Assistant maintenance-log automation: Read Status + Manage Maintenance"), and a paragraph in Upgrade Notes explaining the FALSE backfill so existing users don't see a silent scope widening. **Migration.** Idempotent `_safe_execute` `ALTER TABLE api_keys ADD COLUMN can_manage_maintenance BOOLEAN DEFAULT TRUE` + one-shot `UPDATE api_keys SET can_manage_maintenance = FALSE` guarded by column-existed-before check, mirroring the two prior scope migrations. Works on both SQLite and Postgres — no dialect-specific column type needed since it's just a BOOLEAN. + ### Fixed - **P1S / P1P printer card no longer shows a bogus "Door Closed" badge (#1866, reporter @MartinNYHC)** — The enclosure-door badge in `frontend/src/pages/PrintersPage.tsx` gated visibility on a model whitelist that included `P1S` and `P1P`, but neither has a door sensor: P1S has an enclosure door with no hall sensor behind it, P1P has no enclosure at all. Backend `bambu_mqtt.py:2876-2907` unconditionally parses bit 23 of the `stat` field for every non-X1 model, and the bit stays 0 on P1S/P1P firmware — so the card rendered a permanent green "Door Closed" chip that couldn't reflect real state. **Fix.** Drop `P1S` and `P1P` from the frontend whitelist. Whitelist now reads `['X1C', 'X1', 'X1E', 'X2D', 'P2S', 'H2D', 'H2D Pro', 'H2C', 'H2S']` — the models that ship with a hall sensor for the door (and, on X1 family, top glass). Corrected the stale `X1/P1S/P2S/H2*` comment in `frontend/src/api/client.ts:473` (TypeScript `PrinterStatus` interface) and `backend/app/services/bambu_mqtt.py:295` (`PrinterState` dataclass) to match reality — those comments were what led the whitelist astray in the first place. Backend parsing left as-is: the bit read is cheap and if Bambu ever ships firmware that wires the P-series enclosure into a door sensor, the state gets picked up for free without a Bambuddy change. **Scope.** Two-line frontend whitelist change plus two comment corrections. No test change (no unit tests existed for the frontend gate). No i18n key change (`printers.door.open`/`printers.door.closed` still used for the models that keep the badge). No DB migration. No new permission. No backend behavioural change. diff --git a/backend/app/api/routes/api_keys.py b/backend/app/api/routes/api_keys.py index 71aee90f4..91a0f482a 100644 --- a/backend/app/api/routes/api_keys.py +++ b/backend/app/api/routes/api_keys.py @@ -65,6 +65,7 @@ async def create_api_key( can_read_status=data.can_read_status, can_manage_library=data.can_manage_library, can_manage_inventory=data.can_manage_inventory, + can_manage_maintenance=data.can_manage_maintenance, can_access_cloud=data.can_access_cloud, can_update_energy_cost=data.can_update_energy_cost, printer_ids=data.printer_ids, @@ -86,6 +87,7 @@ async def create_api_key( can_read_status=api_key.can_read_status, can_manage_library=api_key.can_manage_library, can_manage_inventory=api_key.can_manage_inventory, + can_manage_maintenance=api_key.can_manage_maintenance, can_access_cloud=api_key.can_access_cloud, can_update_energy_cost=api_key.can_update_energy_cost, printer_ids=api_key.printer_ids, @@ -139,6 +141,8 @@ async def update_api_key( api_key.can_manage_library = data.can_manage_library if data.can_manage_inventory is not None: api_key.can_manage_inventory = data.can_manage_inventory + if data.can_manage_maintenance is not None: + api_key.can_manage_maintenance = data.can_manage_maintenance if data.can_access_cloud is not None: # Same constraint as create — flipping cloud access on a legacy key # without an owner would be silently broken; reject at the route layer. diff --git a/backend/app/cli.py b/backend/app/cli.py index b04f43800..f30166dcb 100644 --- a/backend/app/cli.py +++ b/backend/app/cli.py @@ -70,6 +70,9 @@ async def kiosk_bootstrap( # commands via the /spoolbuddy/* routes — all gated by # can_manage_inventory now, so the bundled key must opt in. can_manage_inventory=True, + # Kiosk doesn't need maintenance writes; keep it False so the + # bundled key stays minimally scoped (#1832 follow-up). + can_manage_maintenance=False, printer_ids=None, enabled=True, expires_at=None, diff --git a/backend/app/core/auth.py b/backend/app/core/auth.py index eab857480..43f4a1f44 100644 --- a/backend/app/core/auth.py +++ b/backend/app/core/auth.py @@ -49,15 +49,17 @@ logger = logging.getLogger(__name__) # entries also satisfy "not in the allowlist", so they fail closed regardless. # # Mapping rationale (see wiki/features/api-keys.md): -# can_read_status → every ``*_READ`` + camera + stats + system + websocket -# can_queue → queue write ops + archive reprint -# can_control_printer → physical printer + smart-plug control -# can_manage_library → library upload/own + MakerWorld import (separate -# trust level from queue management, hence its own flag) -# admin-only → unmapped (default-deny); covers all create/update/ -# delete of admin resources, settings writes, user/ -# group/api-key/backup admin ops, discovery scan, -# cloud auth, library ALL-ownership perms, purges +# can_read_status → every ``*_READ`` + camera + stats + system + websocket +# can_queue → queue write ops + archive reprint +# can_control_printer → physical printer + smart-plug control +# can_manage_library → library upload/own + MakerWorld import (separate +# trust level from queue management, hence its own flag) +# can_manage_inventory → spool/catalog/forecast writes + SpoolBuddy kiosk writes +# can_manage_maintenance→ per-printer maintenance log/reset + type-catalog CRUD +# admin-only → unmapped (default-deny); covers all create/update/ +# delete of admin resources, settings writes, user/ +# group/api-key/backup admin ops, discovery scan, +# cloud auth, library ALL-ownership perms, purges _APIKEY_SCOPE_BY_PERMISSION: dict[Permission, str] = { # can_read_status — read-only access to status, history, and configuration Permission.PRINTERS_READ: "can_read_status", @@ -138,6 +140,19 @@ _APIKEY_SCOPE_BY_PERMISSION: dict[Permission, str] = { Permission.INVENTORY_UPDATE: "can_manage_inventory", Permission.INVENTORY_DELETE: "can_manage_inventory", Permission.INVENTORY_FORECAST_WRITE: "can_manage_inventory", + # can_manage_maintenance — carved out of the admin denylist so HA-style + # automations can log "cleaned nozzle" / reset a maintenance counter via + # `POST /maintenance/items/{item_id}/perform` without granting broader + # printer control or settings write (#1832 follow-up). Also covers the + # per-printer maintenance CRUD (assign/remove items, edit intervals) and + # the type-catalog CRUD — the type catalog is a config surface (system + # types are auto-seeded, custom types are user-defined), so grouping it + # with the item writes matches the operator mental model of "keys that + # log maintenance can also manage what gets tracked." MAINTENANCE_READ + # stays under can_read_status. + Permission.MAINTENANCE_CREATE: "can_manage_maintenance", + Permission.MAINTENANCE_UPDATE: "can_manage_maintenance", + Permission.MAINTENANCE_DELETE: "can_manage_maintenance", # can_access_cloud — narrow opt-in scope, gated by the router-level # ``_cloud_api_key_gate`` and additionally enforced here so the route- # level ``cloud_caller(Permission.CLOUD_AUTH)`` dep also fails closed @@ -203,9 +218,8 @@ _APIKEY_DENIED_PERMISSIONS: frozenset[Permission] = frozenset( Permission.FILAMENTS_CREATE, Permission.FILAMENTS_UPDATE, Permission.FILAMENTS_DELETE, - Permission.MAINTENANCE_CREATE, - Permission.MAINTENANCE_UPDATE, - Permission.MAINTENANCE_DELETE, + # MAINTENANCE_CREATE / MAINTENANCE_UPDATE / MAINTENANCE_DELETE moved + # to the allowlist under `can_manage_maintenance` (#1832 follow-up). Permission.KPROFILES_CREATE, Permission.KPROFILES_UPDATE, Permission.KPROFILES_DELETE, diff --git a/backend/app/core/database.py b/backend/app/core/database.py index 4cf5aa15c..ce6c335f3 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -2523,6 +2523,25 @@ async def run_migrations(conn): async with conn.begin_nested(): await conn.execute(text("UPDATE api_keys SET can_manage_inventory = can_queue")) + # #1832 follow-up: carve maintenance CRUD out of the admin denylist so + # HA-style automations can log "cleaned nozzle" via API key. Distinct + # from the two backfills above: MAINTENANCE_CREATE / _UPDATE / _DELETE + # were EXPLICITLY denied for every API key under the pre-migration model + # (they were on ``_APIKEY_DENIED_PERMISSIONS``), so no existing + # integration relies on them. Column default TRUE matches the "safe, + # on-by-default" pattern for keys created via the UI going forward; + # existing rows backfill to FALSE so the upgrade path does not silently + # widen scope for keys created before this flag existed. Users opt in + # via Settings → API Keys per key. + column_existed = await _api_keys_column_exists(conn, "can_manage_maintenance") + await _safe_execute( + conn, + "ALTER TABLE api_keys ADD COLUMN can_manage_maintenance BOOLEAN DEFAULT TRUE", + ) + if not column_existed: + async with conn.begin_nested(): + await conn.execute(text("UPDATE api_keys SET can_manage_maintenance = FALSE")) + # Migration: Soft-delete column for trash bin (Issue #1008). Indexed so the # sweeper's "SELECT ... WHERE deleted_at < cutoff" and the trash list's # "WHERE deleted_at IS NOT NULL" stay cheap as the table grows. diff --git a/backend/app/models/api_key.py b/backend/app/models/api_key.py index 51f3ae223..3bfdb0dc0 100644 --- a/backend/app/models/api_key.py +++ b/backend/app/models/api_key.py @@ -36,6 +36,9 @@ class APIKey(Base): can_manage_inventory: Mapped[bool] = mapped_column( Boolean, default=True ) # Inventory write ops (incl. SpoolBuddy kiosk NFC/scale/system) + can_manage_maintenance: Mapped[bool] = mapped_column( + Boolean, default=True + ) # Log/reset per-printer maintenance, edit intervals, manage the type catalog (#1832 follow-up) can_access_cloud: Mapped[bool] = mapped_column(Boolean, default=False) # Read /cloud/* on the owner's behalf # Narrowly-scoped settings write: only POST /settings/electricity-price. # Lets HA/Tibber-style automations push dynamic tariff updates without diff --git a/backend/app/schemas/api_key.py b/backend/app/schemas/api_key.py index 8bad175e1..32067111c 100644 --- a/backend/app/schemas/api_key.py +++ b/backend/app/schemas/api_key.py @@ -12,6 +12,9 @@ class APIKeyCreate(BaseModel): can_read_status: bool = True can_manage_library: bool = True # Upload / rename / delete own library files + MakerWorld import can_manage_inventory: bool = True # Inventory writes — SpoolBuddy NFC/scale/system, manual stock edits via API + can_manage_maintenance: bool = ( + True # Log/reset maintenance items, edit intervals, manage type catalog (#1832 follow-up) + ) can_access_cloud: bool = False # Read /cloud/* on the creator's behalf — default off (#1182) can_update_energy_cost: bool = False # POST /settings/electricity-price only (#1356) printer_ids: list[int] | None = None # null = all printers @@ -27,6 +30,7 @@ class APIKeyUpdate(BaseModel): can_read_status: bool | None = None can_manage_library: bool | None = None can_manage_inventory: bool | None = None + can_manage_maintenance: bool | None = None can_access_cloud: bool | None = None can_update_energy_cost: bool | None = None printer_ids: list[int] | None = None @@ -46,6 +50,7 @@ class APIKeyResponse(BaseModel): can_read_status: bool can_manage_library: bool can_manage_inventory: bool + can_manage_maintenance: bool can_access_cloud: bool can_update_energy_cost: bool printer_ids: list[int] | None diff --git a/backend/tests/integration/test_auth_apikey_rbac.py b/backend/tests/integration/test_auth_apikey_rbac.py index 1ef956b5a..d339f46e7 100644 --- a/backend/tests/integration/test_auth_apikey_rbac.py +++ b/backend/tests/integration/test_auth_apikey_rbac.py @@ -215,6 +215,7 @@ class TestApiKeyScopeAllowlist: "can_control_printer", "can_manage_library", "can_manage_inventory", + "can_manage_maintenance", "can_access_cloud", } used_flags = set(_APIKEY_SCOPE_BY_PERMISSION.values()) @@ -241,6 +242,7 @@ class TestApiKeyScopeAllowlist: "can_control_printer", "can_manage_library", "can_manage_inventory", + "can_manage_maintenance", "can_access_cloud", ], ) @@ -268,12 +270,14 @@ class _FakeApiKey: can_control_printer=False, can_manage_library=False, can_manage_inventory=False, + can_manage_maintenance=False, ): self.can_read_status = can_read_status self.can_queue = can_queue self.can_control_printer = can_control_printer self.can_manage_library = can_manage_library self.can_manage_inventory = can_manage_inventory + self.can_manage_maintenance = can_manage_maintenance class TestCheckApiKeyPermissionsMatrix: @@ -315,6 +319,11 @@ class TestCheckApiKeyPermissionsMatrix: ("INVENTORY_UPDATE", "can_manage_inventory", "update spool / SpoolBuddy kiosk write"), ("INVENTORY_DELETE", "can_manage_inventory", "delete spool record"), ("INVENTORY_FORECAST_WRITE", "can_manage_inventory", "update forecast SKU settings"), + # can_manage_maintenance (#1832 follow-up) — HA "cleaned nozzle" / reset counter + # is the load-bearing use case; MAINTENANCE_UPDATE gates POST /maintenance/items/{id}/perform. + ("MAINTENANCE_CREATE", "can_manage_maintenance", "assign maintenance type to printer"), + ("MAINTENANCE_UPDATE", "can_manage_maintenance", "log maintenance / edit interval"), + ("MAINTENANCE_DELETE", "can_manage_maintenance", "remove custom maintenance item"), ] _ADMIN_CASES = [ @@ -354,7 +363,14 @@ class TestCheckApiKeyPermissionsMatrix: # Wrong flag set, required flag off → 403 (no cross-scope leakage) other_flags = { f - for f in ("can_read_status", "can_queue", "can_control_printer", "can_manage_library") + for f in ( + "can_read_status", + "can_queue", + "can_control_printer", + "can_manage_library", + "can_manage_inventory", + "can_manage_maintenance", + ) if f != required_flag } for other in other_flags: diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 598f1cac0..1d53c80df 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1048,6 +1048,7 @@ export interface APIKey { can_read_status: boolean; can_manage_library: boolean; can_manage_inventory: boolean; + can_manage_maintenance: boolean; can_access_cloud: boolean; can_update_energy_cost: boolean; printer_ids: number[] | null; @@ -1064,6 +1065,7 @@ export interface APIKeyCreate { can_read_status?: boolean; can_manage_library?: boolean; can_manage_inventory?: boolean; + can_manage_maintenance?: boolean; can_access_cloud?: boolean; can_update_energy_cost?: boolean; printer_ids?: number[] | null; @@ -1081,6 +1083,7 @@ export interface APIKeyUpdate { can_read_status?: boolean; can_manage_library?: boolean; can_manage_inventory?: boolean; + can_manage_maintenance?: boolean; can_access_cloud?: boolean; can_update_energy_cost?: boolean; printer_ids?: number[] | null; diff --git a/frontend/src/i18n/locales/de.ts b/frontend/src/i18n/locales/de.ts index 165184141..304c76f96 100644 --- a/frontend/src/i18n/locales/de.ts +++ b/frontend/src/i18n/locales/de.ts @@ -1988,6 +1988,9 @@ export default { manageLibraryDescription: 'Bibliotheksdateien hochladen, umbenennen und löschen; Modelle aus MakerWorld importieren', manageInventory: 'Bestand verwalten', manageInventoryDescription: 'Spulen und Bestandseinträge anlegen, ändern und löschen. Erforderlich für SpoolBuddy-Kioske (NFC-Scan, Waagenmessungen, Kiosk-Systembefehle).', + manageMaintenance: 'Wartung verwalten', + manageMaintenanceDescription: 'Abgeschlossene Wartungen protokollieren, Zähler zurücksetzen, Intervalle bearbeiten und den Wartungstyp-Katalog verwalten. Geeignet für Home-Assistant-Automatisierungen, die „Düse gereinigt" protokollieren, ohne umfassendere Druckersteuerung zu gewähren.', + maintenanceBadge: 'Wartung', libraryBadge: 'Bibliothek', inventoryBadge: 'Bestand', cloudAccess: 'Cloud-Zugriff erlauben', diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index 87e316054..0fb91d184 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -2007,8 +2007,11 @@ export default { manageLibraryDescription: 'Upload, rename, and delete library files; import models from MakerWorld', manageInventory: 'Manage Inventory', manageInventoryDescription: 'Create, update, and delete spools and inventory records. Required for SpoolBuddy kiosks (NFC scan, scale readings, kiosk system commands).', + manageMaintenance: 'Manage Maintenance', + manageMaintenanceDescription: 'Log completed maintenance, reset counters, edit intervals, and manage the maintenance-type catalog. Suited to Home Assistant automations that record "I cleaned the nozzle" without granting broader printer control.', libraryBadge: 'Library', inventoryBadge: 'Inventory', + maintenanceBadge: 'Maintenance', cloudAccess: 'Allow cloud access', cloudAccessDescription: 'Read Bambu Cloud presets and filaments on your behalf. Requires you to be signed into Bambu Cloud.', cloudBadge: 'Cloud', diff --git a/frontend/src/i18n/locales/es.ts b/frontend/src/i18n/locales/es.ts index 8eacc9312..5ee618bbe 100644 --- a/frontend/src/i18n/locales/es.ts +++ b/frontend/src/i18n/locales/es.ts @@ -1991,6 +1991,9 @@ export default { manageLibraryDescription: 'Subir, renombrar y eliminar archivos de la biblioteca; importar modelos desde MakerWorld', manageInventory: 'Gestionar inventario', manageInventoryDescription: 'Crear, actualizar y eliminar bobinas y registros de inventario. Necesario para los quioscos SpoolBuddy (escaneo NFC, lecturas de balanza, comandos del sistema del quiosco).', + manageMaintenance: 'Gestionar mantenimiento', + manageMaintenanceDescription: 'Registrar mantenimientos completados, restablecer contadores, editar intervalos y gestionar el catálogo de tipos de mantenimiento. Ideal para automatizaciones de Home Assistant que registran "limpié la boquilla" sin conceder un control más amplio de la impresora.', + maintenanceBadge: 'Mantenimiento', libraryBadge: 'Biblioteca', inventoryBadge: 'Inventario', cloudAccess: 'Permitir el acceso a la nube', diff --git a/frontend/src/i18n/locales/fr.ts b/frontend/src/i18n/locales/fr.ts index 699f1bd26..9ed0b871a 100644 --- a/frontend/src/i18n/locales/fr.ts +++ b/frontend/src/i18n/locales/fr.ts @@ -1944,6 +1944,9 @@ export default { manageLibraryDescription: 'Téléverser, renommer et supprimer des fichiers de la bibliothèque ; importer des modèles depuis MakerWorld', manageInventory: 'Gérer l\'inventaire', manageInventoryDescription: 'Créer, modifier et supprimer des bobines et des entrées d\'inventaire. Requis pour les bornes SpoolBuddy (scan NFC, lectures de balance, commandes système de la borne).', + manageMaintenance: 'Gérer la maintenance', + manageMaintenanceDescription: 'Enregistrer les maintenances effectuées, réinitialiser les compteurs, modifier les intervalles et gérer le catalogue des types de maintenance. Adapté aux automatisations Home Assistant qui consignent "J\'ai nettoyé la buse" sans accorder un contrôle plus large de l\'imprimante.', + maintenanceBadge: 'Maintenance', libraryBadge: 'Bibliothèque', inventoryBadge: 'Inventaire', cloudAccess: 'Autoriser l\'accès cloud', diff --git a/frontend/src/i18n/locales/it.ts b/frontend/src/i18n/locales/it.ts index 6ac631c17..4bf1cf03d 100644 --- a/frontend/src/i18n/locales/it.ts +++ b/frontend/src/i18n/locales/it.ts @@ -1944,6 +1944,9 @@ export default { manageLibraryDescription: 'Carica, rinomina ed elimina file della libreria; importa modelli da MakerWorld', manageInventory: 'Gestisci inventario', manageInventoryDescription: 'Crea, aggiorna ed elimina bobine e voci di inventario. Necessario per i chioschi SpoolBuddy (scansione NFC, letture della bilancia, comandi di sistema del chiosco).', + manageMaintenance: 'Gestisci manutenzione', + manageMaintenanceDescription: 'Registra le manutenzioni completate, azzera i contatori, modifica gli intervalli e gestisci il catalogo dei tipi di manutenzione. Adatto alle automazioni Home Assistant che registrano "ho pulito l\'ugello" senza concedere un controllo più ampio della stampante.', + maintenanceBadge: 'Manutenzione', libraryBadge: 'Libreria', inventoryBadge: 'Inventario', cloudAccess: 'Consenti accesso cloud', diff --git a/frontend/src/i18n/locales/ja.ts b/frontend/src/i18n/locales/ja.ts index 88bf3a609..7ed9b1322 100644 --- a/frontend/src/i18n/locales/ja.ts +++ b/frontend/src/i18n/locales/ja.ts @@ -1987,6 +1987,9 @@ export default { manageLibraryDescription: 'ライブラリファイルのアップロード、名前変更、削除。MakerWorld からのモデルインポート。', manageInventory: '在庫の管理', manageInventoryDescription: 'スプールと在庫レコードの作成、更新、削除。SpoolBuddy キオスク(NFC スキャン、はかり読み取り、キオスクのシステムコマンド)に必要です。', + manageMaintenance: 'メンテナンスの管理', + manageMaintenanceDescription: '完了したメンテナンスの記録、カウンターのリセット、間隔の編集、メンテナンス種類カタログの管理。より広範なプリンター制御を許可することなく「ノズルをクリーニングした」を記録する Home Assistant オートメーションに適しています。', + maintenanceBadge: 'メンテナンス', libraryBadge: 'ライブラリ', inventoryBadge: '在庫', cloudAccess: 'クラウドアクセスを許可', diff --git a/frontend/src/i18n/locales/ko.ts b/frontend/src/i18n/locales/ko.ts index ce763c72e..70ea967d1 100644 --- a/frontend/src/i18n/locales/ko.ts +++ b/frontend/src/i18n/locales/ko.ts @@ -1873,6 +1873,9 @@ export default { manageLibraryDescription: '라이브러리 파일 업로드, 이름 변경 및 삭제; MakerWorld에서 모델 가져오기', manageInventory: '재고 관리', manageInventoryDescription: '스풀 및 재고 레코드 생성, 업데이트 및 삭제. SpoolBuddy 키오스크(NFC 스캔, 저울 측정값, 키오스크 시스템 명령)에 필요합니다.', + manageMaintenance: '유지보수 관리', + manageMaintenanceDescription: '완료된 유지보수 기록, 카운터 재설정, 주기 편집, 유지보수 유형 카탈로그 관리. 프린터 제어를 더 광범위하게 허용하지 않고 "노즐을 청소했다"를 기록하는 Home Assistant 자동화에 적합합니다.', + maintenanceBadge: '유지보수', libraryBadge: '라이브러리', inventoryBadge: '재고', cloudAccess: '클라우드 접근 허용', diff --git a/frontend/src/i18n/locales/pt-BR.ts b/frontend/src/i18n/locales/pt-BR.ts index 5838c7a69..80c0b224b 100644 --- a/frontend/src/i18n/locales/pt-BR.ts +++ b/frontend/src/i18n/locales/pt-BR.ts @@ -1944,6 +1944,9 @@ export default { manageLibraryDescription: 'Enviar, renomear e excluir arquivos da biblioteca; importar modelos do MakerWorld', manageInventory: 'Gerenciar estoque', manageInventoryDescription: 'Criar, atualizar e excluir bobinas e registros de estoque. Necessário para quiosques SpoolBuddy (escaneamento NFC, leituras da balança, comandos de sistema do quiosque).', + manageMaintenance: 'Gerenciar manutenção', + manageMaintenanceDescription: 'Registrar manutenções concluídas, redefinir contadores, editar intervalos e gerenciar o catálogo de tipos de manutenção. Adequado a automações do Home Assistant que registram "limpei o bico" sem conceder controle mais amplo da impressora.', + maintenanceBadge: 'Manutenção', libraryBadge: 'Biblioteca', inventoryBadge: 'Estoque', cloudAccess: 'Permitir acesso à nuvem', diff --git a/frontend/src/i18n/locales/tr.ts b/frontend/src/i18n/locales/tr.ts index e3ab1659a..bc2cf041e 100644 --- a/frontend/src/i18n/locales/tr.ts +++ b/frontend/src/i18n/locales/tr.ts @@ -1992,6 +1992,9 @@ export default { manageLibraryDescription: "Kütüphane dosyalarını yükle, yeniden adlandır ve sil; MakerWorld'den model içe aktar", manageInventory: 'Envanteri Yönet', manageInventoryDescription: 'Makaraları ve envanter kayıtlarını oluştur, güncelle ve sil. SpoolBuddy kiosklar (NFC tarama, tartı okumaları, kiosk sistem komutları) için gereklidir.', + manageMaintenance: 'Bakımı yönet', + manageMaintenanceDescription: 'Tamamlanan bakımları kaydet, sayaçları sıfırla, aralıkları düzenle ve bakım türü kataloğunu yönet. Daha geniş yazıcı kontrolü vermeden "nozulu temizledim"i kaydeden Home Assistant otomasyonlarına uygundur.', + maintenanceBadge: 'Bakım', libraryBadge: 'Kütüphane', inventoryBadge: 'Envanter', cloudAccess: 'Bulut erişimine izin ver', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index fb291173f..22371b36f 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -1989,6 +1989,9 @@ export default { manageLibraryDescription: '上传、重命名和删除资料库文件;从 MakerWorld 导入模型', manageInventory: '管理库存', manageInventoryDescription: '创建、更新和删除耗材盘以及库存记录。SpoolBuddy 终端(NFC 扫描、秤读取、终端系统命令)需要此权限。', + manageMaintenance: '管理维护', + manageMaintenanceDescription: '记录已完成的维护、重置计数器、编辑间隔并管理维护类型目录。适用于在不授予更广泛打印机控制权限的情况下记录"我清洁了喷嘴"的 Home Assistant 自动化。', + maintenanceBadge: '维护', libraryBadge: '资料库', inventoryBadge: '库存', cloudAccess: '允许云端访问', diff --git a/frontend/src/i18n/locales/zh-TW.ts b/frontend/src/i18n/locales/zh-TW.ts index 42f7cd421..5acb0b827 100644 --- a/frontend/src/i18n/locales/zh-TW.ts +++ b/frontend/src/i18n/locales/zh-TW.ts @@ -1989,6 +1989,9 @@ export default { manageLibraryDescription: '上傳、重新命名與刪除資料庫檔案;從 MakerWorld 匯入模型', manageInventory: '管理庫存', manageInventoryDescription: '建立、更新與刪除耗材盤與庫存記錄。SpoolBuddy 終端(NFC 掃描、秤讀取、終端系統指令)需要此權限。', + manageMaintenance: '管理維護', + manageMaintenanceDescription: '記錄已完成的維護、重設計數器、編輯間隔並管理維護類型目錄。適用於在不授予更廣泛印表機控制權限的情況下記錄「我清潔了噴嘴」的 Home Assistant 自動化。', + maintenanceBadge: '維護', libraryBadge: '資料庫', inventoryBadge: '庫存', cloudAccess: '允許雲端存取', diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 290f10662..14c0b83ed 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -234,6 +234,7 @@ export function SettingsPage() { can_read_status: true, can_manage_library: true, can_manage_inventory: true, + can_manage_maintenance: true, can_access_cloud: false, can_update_energy_cost: false, }); @@ -393,7 +394,7 @@ export function SettingsPage() { }); const createAPIKeyMutation = useMutation({ - mutationFn: (data: { name: string; can_queue: boolean; can_control_printer: boolean; can_read_status: boolean; can_manage_library: boolean; can_manage_inventory: boolean; can_access_cloud: boolean }) => + mutationFn: (data: { name: string; can_queue: boolean; can_control_printer: boolean; can_read_status: boolean; can_manage_library: boolean; can_manage_inventory: boolean; can_manage_maintenance: boolean; can_access_cloud: boolean }) => api.createAPIKey(data), onSuccess: (data) => { setCreatedAPIKey(data.key || null); @@ -3858,6 +3859,18 @@ export function SettingsPage() {
{t('settings.manageInventoryDescription')}
+