Просмотр исходного кода

feat(api-keys): can_manage_maintenance scope for HA-style automations (#1832 follow-up)

    Carve MAINTENANCE_CREATE/UPDATE/DELETE out of the admin denylist so
    HA automations can log "cleaned nozzle" / reset a counter via API key
    without granting broader printer control. Follows the same shape as
    can_manage_library and can_manage_inventory: new column, allowlist
    entry, UI checkbox, wiki row, RBAC test coverage.

    Distinct backfill: these perms were EXPLICITLY denied for every API
    key before this change (no existing integration relies on them), so
    existing rows migrate to FALSE — no silent scope widening on upgrade.
    New keys default to TRUE, matching the safe-on-by-default pattern.
    Bundled SpoolBuddy kiosk key gets False explicitly (kiosk doesn't need it).
maziggy 2 месяцев назад
Родитель
Сommit
b8b5aaa977

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
CHANGELOG.md


+ 4 - 0
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.

+ 3 - 0
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,

+ 26 - 12
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,

+ 19 - 0
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.

+ 3 - 0
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

+ 5 - 0
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

+ 17 - 1
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:

+ 3 - 0
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;

+ 3 - 0
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',

+ 3 - 0
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',

+ 3 - 0
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',

+ 3 - 0
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',

+ 3 - 0
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',

+ 3 - 0
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: 'クラウドアクセスを許可',

+ 3 - 0
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: '클라우드 접근 허용',

+ 3 - 0
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',

+ 3 - 0
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',

+ 3 - 0
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: '允许云端访问',

+ 3 - 0
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: '允許雲端存取',

+ 17 - 1
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() {
                           <p className="text-xs text-bambu-gray">{t('settings.manageInventoryDescription')}</p>
                         </div>
                       </label>
+                      <label className="flex items-center gap-3 cursor-pointer">
+                        <input
+                          type="checkbox"
+                          checked={newAPIKeyPermissions.can_manage_maintenance}
+                          onChange={(e) => setNewAPIKeyPermissions(prev => ({ ...prev, can_manage_maintenance: e.target.checked }))}
+                          className="w-4 h-4 text-bambu-green rounded border-bambu-dark-tertiary bg-bambu-dark focus:ring-bambu-green"
+                        />
+                        <div>
+                          <span className="text-white">{t('settings.manageMaintenance')}</span>
+                          <p className="text-xs text-bambu-gray">{t('settings.manageMaintenanceDescription')}</p>
+                        </div>
+                      </label>
                       <label className="flex items-center gap-3 cursor-pointer">
                         <input
                           type="checkbox"
@@ -3945,6 +3958,9 @@ export function SettingsPage() {
                             {key.can_manage_inventory && (
                               <span className="px-1.5 py-0.5 bg-pink-500/20 text-pink-400 rounded">{t('settings.inventoryBadge')}</span>
                             )}
+                            {key.can_manage_maintenance && (
+                              <span className="px-1.5 py-0.5 bg-teal-500/20 text-teal-400 rounded">{t('settings.maintenanceBadge')}</span>
+                            )}
                             {key.can_access_cloud && (
                               <span className="px-1.5 py-0.5 bg-purple-500/20 text-purple-400 rounded">{t('settings.cloudBadge', 'Cloud')}</span>
                             )}

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
static/assets/index-DDY1oW0X.js


+ 1 - 1
static/index.html

@@ -26,7 +26,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DiaNe89t.js"></script>
+    <script type="module" crossorigin src="/assets/index-DDY1oW0X.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BxVhuRti.css">
   </head>
   <body>

Некоторые файлы не были показаны из-за большого количества измененных файлов