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

fix(auth): allow API keys to delete/edit archives via new can_manage_archives scope (#1888)

DELETE /api/v1/archives/{id} rejected every API key with 403
"API keys cannot be used for administrative operations", regardless of
the print's owner or the key's scopes. ARCHIVES_DELETE_ALL/_OWN (and the
create/update variants) were on the denylist and absent from the scope
allowlist, so require_ownership_permission fell through to the generic
admin-denied 403 — the whole archive-management surface was unreachable
for API keys. Same regression class as the #1832 library/maintenance
carve-outs.

Add a can_manage_archives per-key scope: ARCHIVES_CREATE, ARCHIVES_
UPDATE_OWN/_ALL and ARCHIVES_DELETE_OWN/_ALL move from the denylist to
the allowlist under it (OWN and ALL fold into the same scope, matching
can_manage_library). ARCHIVES_PURGE stays admin-only — it drops the
print's Quick Stats contribution, mirroring LIBRARY_PURGE. Column
defaults TRUE for UI-created keys; existing rows backfill to FALSE so the
upgrade never silently widens scope. Bundled SpoolBuddy kiosk key stays
minimally scoped (False). Migration is dialect-agnostic and verified on
fresh SQLite and Postgres 17.

Adds the Settings API-key toggle + badge (11-locale i18n) and extends the
RBAC scope matrix to cover all five archive-management permissions.
maziggy 2 месяцев назад
Родитель
Сommit
6358e9544e

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


+ 4 - 0
backend/app/api/routes/api_keys.py

@@ -66,6 +66,7 @@ async def create_api_key(
         can_manage_library=data.can_manage_library,
         can_manage_inventory=data.can_manage_inventory,
         can_manage_maintenance=data.can_manage_maintenance,
+        can_manage_archives=data.can_manage_archives,
         can_access_cloud=data.can_access_cloud,
         can_update_energy_cost=data.can_update_energy_cost,
         printer_ids=data.printer_ids,
@@ -88,6 +89,7 @@ async def create_api_key(
         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_manage_archives=api_key.can_manage_archives,
         can_access_cloud=api_key.can_access_cloud,
         can_update_energy_cost=api_key.can_update_energy_cost,
         printer_ids=api_key.printer_ids,
@@ -143,6 +145,8 @@ async def update_api_key(
         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_manage_archives is not None:
+        api_key.can_manage_archives = data.can_manage_archives
     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

@@ -73,6 +73,9 @@ async def kiosk_bootstrap(
             # Kiosk doesn't need maintenance writes; keep it False so the
             # bundled key stays minimally scoped (#1832 follow-up).
             can_manage_maintenance=False,
+            # Kiosk doesn't manage print archives either — keep it minimally
+            # scoped (#1888).
+            can_manage_archives=False,
             printer_ids=None,
             enabled=True,
             expires_at=None,

+ 21 - 5
backend/app/core/auth.py

@@ -153,6 +153,20 @@ _APIKEY_SCOPE_BY_PERMISSION: dict[Permission, str] = {
     Permission.MAINTENANCE_CREATE: "can_manage_maintenance",
     Permission.MAINTENANCE_UPDATE: "can_manage_maintenance",
     Permission.MAINTENANCE_DELETE: "can_manage_maintenance",
+    # can_manage_archives — print-history curation. Carved out of the admin
+    # denylist so automations can prune old prints via API key (#1888): the
+    # archive delete/update routes gate on
+    # ``require_ownership_permission(ARCHIVES_*_ALL, ARCHIVES_*_OWN)``, which
+    # resolves the ALL permission for API keys (no per-row ownership identity,
+    # same as can_queue / can_manage_library), so OWN and ALL map to the same
+    # scope. ARCHIVES_PURGE stays admin-only (see denylist) as a genuinely
+    # destructive op that drops the stats contribution, mirroring LIBRARY_PURGE.
+    # ARCHIVES_REPRINT_* stays under can_queue (it enqueues a print).
+    Permission.ARCHIVES_CREATE: "can_manage_archives",
+    Permission.ARCHIVES_UPDATE_OWN: "can_manage_archives",
+    Permission.ARCHIVES_UPDATE_ALL: "can_manage_archives",
+    Permission.ARCHIVES_DELETE_OWN: "can_manage_archives",
+    Permission.ARCHIVES_DELETE_ALL: "can_manage_archives",
     # 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
@@ -200,11 +214,13 @@ _APIKEY_DENIED_PERMISSIONS: frozenset[Permission] = frozenset(
         Permission.PRINTERS_CREATE,
         Permission.PRINTERS_UPDATE,
         Permission.PRINTERS_DELETE,
-        Permission.ARCHIVES_CREATE,
-        Permission.ARCHIVES_UPDATE_OWN,
-        Permission.ARCHIVES_UPDATE_ALL,
-        Permission.ARCHIVES_DELETE_OWN,
-        Permission.ARCHIVES_DELETE_ALL,
+        # ARCHIVES_CREATE / _UPDATE_OWN / _UPDATE_ALL / _DELETE_OWN /
+        # _DELETE_ALL moved to the allowlist under `can_manage_archives`
+        # (#1888) — split between allow/deny made the whole archive-management
+        # surface unreachable for API keys via `require_ownership_permission`
+        # (same regression class as the library/maintenance carve-outs in
+        # #1832). ARCHIVES_PURGE stays denied as a genuinely destructive op
+        # that drops the print's stats contribution.
         Permission.ARCHIVES_PURGE,
         # LIBRARY_UPDATE_ALL / LIBRARY_DELETE_ALL moved to the allowlist
         # under `can_manage_library` (#1832) — split between allow/deny made

+ 19 - 0
backend/app/core/database.py

@@ -2542,6 +2542,25 @@ async def run_migrations(conn):
         async with conn.begin_nested():
             await conn.execute(text("UPDATE api_keys SET can_manage_maintenance = FALSE"))
 
+    # #1888: carve archive CRUD (create/update/delete — NOT purge) out of the
+    # admin denylist so automations can prune old prints via API key. Same
+    # shape and reasoning as can_manage_maintenance above: ARCHIVES_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 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. BOOLEAN is valid
+    # on both SQLite and Postgres, so no dialect branch is needed.
+    column_existed = await _api_keys_column_exists(conn, "can_manage_archives")
+    await _safe_execute(
+        conn,
+        "ALTER TABLE api_keys ADD COLUMN can_manage_archives BOOLEAN DEFAULT TRUE",
+    )
+    if not column_existed:
+        async with conn.begin_nested():
+            await conn.execute(text("UPDATE api_keys SET can_manage_archives = 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

@@ -39,6 +39,9 @@ class APIKey(Base):
     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_manage_archives: Mapped[bool] = mapped_column(
+        Boolean, default=True
+    )  # Create/update/delete print archives (not purge) (#1888)
     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

+ 3 - 0
backend/app/schemas/api_key.py

@@ -15,6 +15,7 @@ class APIKeyCreate(BaseModel):
     can_manage_maintenance: bool = (
         True  # Log/reset maintenance items, edit intervals, manage type catalog (#1832 follow-up)
     )
+    can_manage_archives: bool = True  # Create/update/delete print archives — not purge (#1888)
     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
@@ -31,6 +32,7 @@ class APIKeyUpdate(BaseModel):
     can_manage_library: bool | None = None
     can_manage_inventory: bool | None = None
     can_manage_maintenance: bool | None = None
+    can_manage_archives: bool | None = None
     can_access_cloud: bool | None = None
     can_update_energy_cost: bool | None = None
     printer_ids: list[int] | None = None
@@ -51,6 +53,7 @@ class APIKeyResponse(BaseModel):
     can_manage_library: bool
     can_manage_inventory: bool
     can_manage_maintenance: bool
+    can_manage_archives: bool
     can_access_cloud: bool
     can_update_energy_cost: bool
     printer_ids: list[int] | None

+ 22 - 0
backend/tests/integration/test_auth_apikey_rbac.py

@@ -158,6 +158,10 @@ class TestApiKeyDenylistIntegrity:
             Permission.PRINTERS_READ,
             Permission.PRINTERS_CONTROL,
             Permission.ARCHIVES_READ,
+            # #1888: archive delete/update moved off the denylist to the
+            # can_manage_archives allowlist scope; must not be denied.
+            Permission.ARCHIVES_DELETE_ALL,
+            Permission.ARCHIVES_UPDATE_ALL,
             # SpoolBuddy kiosk reads settings (e.g. language) via API key — must stay allowed.
             Permission.SETTINGS_READ,
         }
@@ -216,6 +220,7 @@ class TestApiKeyScopeAllowlist:
             "can_manage_library",
             "can_manage_inventory",
             "can_manage_maintenance",
+            "can_manage_archives",
             "can_access_cloud",
         }
         used_flags = set(_APIKEY_SCOPE_BY_PERMISSION.values())
@@ -243,6 +248,7 @@ class TestApiKeyScopeAllowlist:
             "can_manage_library",
             "can_manage_inventory",
             "can_manage_maintenance",
+            "can_manage_archives",
             "can_access_cloud",
         ],
     )
@@ -271,6 +277,7 @@ class _FakeApiKey:
         can_manage_library=False,
         can_manage_inventory=False,
         can_manage_maintenance=False,
+        can_manage_archives=False,
     ):
         self.can_read_status = can_read_status
         self.can_queue = can_queue
@@ -278,6 +285,7 @@ class _FakeApiKey:
         self.can_manage_library = can_manage_library
         self.can_manage_inventory = can_manage_inventory
         self.can_manage_maintenance = can_manage_maintenance
+        self.can_manage_archives = can_manage_archives
 
 
 class TestCheckApiKeyPermissionsMatrix:
@@ -324,6 +332,15 @@ class TestCheckApiKeyPermissionsMatrix:
         ("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"),
+        # can_manage_archives (#1888) — prune print history via API key. OWN and
+        # ALL ownership variants both fold into the same scope (API keys have no
+        # per-row ownership identity), matching the can_manage_library shape.
+        # ARCHIVES_PURGE stays admin-only (see _ADMIN_CASES).
+        ("ARCHIVES_CREATE", "can_manage_archives", "create an archive"),
+        ("ARCHIVES_UPDATE_OWN", "can_manage_archives", "edit own archive"),
+        ("ARCHIVES_UPDATE_ALL", "can_manage_archives", "edit any archive"),
+        ("ARCHIVES_DELETE_OWN", "can_manage_archives", "delete own archive"),
+        ("ARCHIVES_DELETE_ALL", "can_manage_archives", "delete any archive"),
     ]
 
     _ADMIN_CASES = [
@@ -339,6 +356,10 @@ class TestCheckApiKeyPermissionsMatrix:
         # LIBRARY_DELETE_ALL / LIBRARY_UPDATE_ALL moved to can_manage_library
         # under #1832 — covered by the _SCOPE_CASES matrix above.
         "LIBRARY_PURGE",
+        # ARCHIVES_PURGE stays admin-only even though the rest of archive
+        # management moved to can_manage_archives under #1888 — it drops the
+        # print's stats contribution, mirroring LIBRARY_PURGE.
+        "ARCHIVES_PURGE",
         "DISCOVERY_SCAN",
     ]
 
@@ -370,6 +391,7 @@ class TestCheckApiKeyPermissionsMatrix:
                 "can_manage_library",
                 "can_manage_inventory",
                 "can_manage_maintenance",
+                "can_manage_archives",
             )
             if f != required_flag
         }

+ 3 - 0
frontend/src/api/client.ts

@@ -1049,6 +1049,7 @@ export interface APIKey {
   can_manage_library: boolean;
   can_manage_inventory: boolean;
   can_manage_maintenance: boolean;
+  can_manage_archives: boolean;
   can_access_cloud: boolean;
   can_update_energy_cost: boolean;
   printer_ids: number[] | null;
@@ -1066,6 +1067,7 @@ export interface APIKeyCreate {
   can_manage_library?: boolean;
   can_manage_inventory?: boolean;
   can_manage_maintenance?: boolean;
+  can_manage_archives?: boolean;
   can_access_cloud?: boolean;
   can_update_energy_cost?: boolean;
   printer_ids?: number[] | null;
@@ -1084,6 +1086,7 @@ export interface APIKeyUpdate {
   can_manage_library?: boolean;
   can_manage_inventory?: boolean;
   can_manage_maintenance?: boolean;
+  can_manage_archives?: boolean;
   can_access_cloud?: boolean;
   can_update_energy_cost?: boolean;
   printer_ids?: number[] | null;

+ 3 - 0
frontend/src/i18n/locales/de.ts

@@ -1990,7 +1990,10 @@ export default {
     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.',
+    manageArchives: 'Archive verwalten',
+    manageArchivesDescription: 'Druckarchive bearbeiten und löschen, einschließlich des Entfernens alter Drucke. Umfasst nicht das Bereinigen ihres Statistikbeitrags. Geeignet für Automatisierungen, die den Druckverlauf ausdünnen.',
     maintenanceBadge: 'Wartung',
+    archivesBadge: 'Archive',
     libraryBadge: 'Bibliothek',
     inventoryBadge: 'Bestand',
     cloudAccess: 'Cloud-Zugriff erlauben',

+ 3 - 0
frontend/src/i18n/locales/en.ts

@@ -2009,9 +2009,12 @@ export default {
     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.',
+    manageArchives: 'Manage Archives',
+    manageArchivesDescription: 'Edit and delete print archives, including removing old prints. Does not include purging their statistics contribution. Suited to automations that prune the print history.',
     libraryBadge: 'Library',
     inventoryBadge: 'Inventory',
     maintenanceBadge: 'Maintenance',
+    archivesBadge: 'Archives',
     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

@@ -1993,7 +1993,10 @@ export default {
     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.',
+    manageArchives: 'Gestionar archivos de impresión',
+    manageArchivesDescription: 'Editar y eliminar archivos de impresión, incluida la eliminación de impresiones antiguas. No incluye la purga de su contribución a las estadísticas. Ideal para automatizaciones que depuran el historial de impresión.',
     maintenanceBadge: 'Mantenimiento',
+    archivesBadge: 'Archivos',
     libraryBadge: 'Biblioteca',
     inventoryBadge: 'Inventario',
     cloudAccess: 'Permitir el acceso a la nube',

+ 3 - 0
frontend/src/i18n/locales/fr.ts

@@ -1946,7 +1946,10 @@ export default {
     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.',
+    manageArchives: 'Gérer les archives',
+    manageArchivesDescription: 'Modifier et supprimer les archives d\'impression, y compris la suppression des anciennes impressions. N\'inclut pas la purge de leur contribution aux statistiques. Adapté aux automatisations qui élaguent l\'historique d\'impression.',
     maintenanceBadge: 'Maintenance',
+    archivesBadge: 'Archives',
     libraryBadge: 'Bibliothèque',
     inventoryBadge: 'Inventaire',
     cloudAccess: 'Autoriser l\'accès cloud',

+ 3 - 0
frontend/src/i18n/locales/it.ts

@@ -1946,7 +1946,10 @@ export default {
     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.',
+    manageArchives: 'Gestisci archivi',
+    manageArchivesDescription: 'Modifica ed elimina gli archivi di stampa, inclusa la rimozione delle vecchie stampe. Non include l\'eliminazione del loro contributo alle statistiche. Adatto alle automazioni che sfoltiscono la cronologia di stampa.',
     maintenanceBadge: 'Manutenzione',
+    archivesBadge: 'Archivi',
     libraryBadge: 'Libreria',
     inventoryBadge: 'Inventario',
     cloudAccess: 'Consenti accesso cloud',

+ 3 - 0
frontend/src/i18n/locales/ja.ts

@@ -1989,7 +1989,10 @@ export default {
     manageInventoryDescription: 'スプールと在庫レコードの作成、更新、削除。SpoolBuddy キオスク(NFC スキャン、はかり読み取り、キオスクのシステムコマンド)に必要です。',
     manageMaintenance: 'メンテナンスの管理',
     manageMaintenanceDescription: '完了したメンテナンスの記録、カウンターのリセット、間隔の編集、メンテナンス種類カタログの管理。より広範なプリンター制御を許可することなく「ノズルをクリーニングした」を記録する Home Assistant オートメーションに適しています。',
+    manageArchives: 'アーカイブの管理',
+    manageArchivesDescription: '古い印刷の削除を含む、印刷アーカイブの編集と削除。統計への寄与の削除は含まれません。印刷履歴を整理するオートメーションに適しています。',
     maintenanceBadge: 'メンテナンス',
+    archivesBadge: 'アーカイブ',
     libraryBadge: 'ライブラリ',
     inventoryBadge: '在庫',
     cloudAccess: 'クラウドアクセスを許可',

+ 3 - 0
frontend/src/i18n/locales/ko.ts

@@ -1875,7 +1875,10 @@ export default {
     manageInventoryDescription: '스풀 및 재고 레코드 생성, 업데이트 및 삭제. SpoolBuddy 키오스크(NFC 스캔, 저울 측정값, 키오스크 시스템 명령)에 필요합니다.',
     manageMaintenance: '유지보수 관리',
     manageMaintenanceDescription: '완료된 유지보수 기록, 카운터 재설정, 주기 편집, 유지보수 유형 카탈로그 관리. 프린터 제어를 더 광범위하게 허용하지 않고 "노즐을 청소했다"를 기록하는 Home Assistant 자동화에 적합합니다.',
+    manageArchives: '아카이브 관리',
+    manageArchivesDescription: '오래된 출력물 제거를 포함하여 출력 아카이브를 편집하고 삭제합니다. 통계 기여 항목의 제거는 포함되지 않습니다. 출력 기록을 정리하는 자동화에 적합합니다.',
     maintenanceBadge: '유지보수',
+    archivesBadge: '아카이브',
     libraryBadge: '라이브러리',
     inventoryBadge: '재고',
     cloudAccess: '클라우드 접근 허용',

+ 3 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -1946,7 +1946,10 @@ export default {
     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.',
+    manageArchives: 'Gerenciar arquivos de impressão',
+    manageArchivesDescription: 'Editar e excluir arquivos de impressão, incluindo a remoção de impressões antigas. Não inclui a limpeza da contribuição deles para as estatísticas. Adequado a automações que reduzem o histórico de impressão.',
     maintenanceBadge: 'Manutenção',
+    archivesBadge: 'Arquivos',
     libraryBadge: 'Biblioteca',
     inventoryBadge: 'Estoque',
     cloudAccess: 'Permitir acesso à nuvem',

+ 3 - 0
frontend/src/i18n/locales/tr.ts

@@ -1994,7 +1994,10 @@ export default {
     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.',
+    manageArchives: 'Arşivleri yönet',
+    manageArchivesDescription: 'Eski baskıların kaldırılması dahil olmak üzere baskı arşivlerini düzenleyin ve silin. İstatistik katkılarının temizlenmesini içermez. Baskı geçmişini budayan otomasyonlar için uygundur.',
     maintenanceBadge: 'Bakım',
+    archivesBadge: 'Arşivler',
     libraryBadge: 'Kütüphane',
     inventoryBadge: 'Envanter',
     cloudAccess: 'Bulut erişimine izin ver',

+ 3 - 0
frontend/src/i18n/locales/zh-CN.ts

@@ -1991,7 +1991,10 @@ export default {
     manageInventoryDescription: '创建、更新和删除耗材盘以及库存记录。SpoolBuddy 终端(NFC 扫描、秤读取、终端系统命令)需要此权限。',
     manageMaintenance: '管理维护',
     manageMaintenanceDescription: '记录已完成的维护、重置计数器、编辑间隔并管理维护类型目录。适用于在不授予更广泛打印机控制权限的情况下记录"我清洁了喷嘴"的 Home Assistant 自动化。',
+    manageArchives: '管理打印存档',
+    manageArchivesDescription: '编辑和删除打印存档,包括移除旧的打印。不包括清除其统计数据贡献。适用于精简打印历史的自动化。',
     maintenanceBadge: '维护',
+    archivesBadge: '存档',
     libraryBadge: '资料库',
     inventoryBadge: '库存',
     cloudAccess: '允许云端访问',

+ 3 - 0
frontend/src/i18n/locales/zh-TW.ts

@@ -1991,7 +1991,10 @@ export default {
     manageInventoryDescription: '建立、更新與刪除耗材盤與庫存記錄。SpoolBuddy 終端(NFC 掃描、秤讀取、終端系統指令)需要此權限。',
     manageMaintenance: '管理維護',
     manageMaintenanceDescription: '記錄已完成的維護、重設計數器、編輯間隔並管理維護類型目錄。適用於在不授予更廣泛印表機控制權限的情況下記錄「我清潔了噴嘴」的 Home Assistant 自動化。',
+    manageArchives: '管理列印封存',
+    manageArchivesDescription: '編輯和刪除列印封存,包括移除舊的列印。不包括清除其統計資料貢獻。適用於精簡列印歷史的自動化。',
     maintenanceBadge: '維護',
+    archivesBadge: '封存',
     libraryBadge: '資料庫',
     inventoryBadge: '庫存',
     cloudAccess: '允許雲端存取',

+ 17 - 1
frontend/src/pages/SettingsPage.tsx

@@ -235,6 +235,7 @@ export function SettingsPage() {
     can_manage_library: true,
     can_manage_inventory: true,
     can_manage_maintenance: true,
+    can_manage_archives: true,
     can_access_cloud: false,
     can_update_energy_cost: false,
   });
@@ -394,7 +395,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_manage_maintenance: 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_manage_archives: boolean; can_access_cloud: boolean }) =>
       api.createAPIKey(data),
     onSuccess: (data) => {
       setCreatedAPIKey(data.key || null);
@@ -3871,6 +3872,18 @@ export function SettingsPage() {
                           <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"
+                          checked={newAPIKeyPermissions.can_manage_archives}
+                          onChange={(e) => setNewAPIKeyPermissions(prev => ({ ...prev, can_manage_archives: 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.manageArchives')}</span>
+                          <p className="text-xs text-bambu-gray">{t('settings.manageArchivesDescription')}</p>
+                        </div>
+                      </label>
                       <label className="flex items-center gap-3 cursor-pointer">
                         <input
                           type="checkbox"
@@ -3961,6 +3974,9 @@ export function SettingsPage() {
                             {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_manage_archives && (
+                              <span className="px-1.5 py-0.5 bg-indigo-500/20 text-indigo-400 rounded">{t('settings.archivesBadge')}</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-0IaClY0k.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-BwslAeY1.js"></script>
+    <script type="module" crossorigin src="/assets/index-0IaClY0k.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BxVhuRti.css">
   </head>
   <body>

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