Browse Source

Add spool inventory and print archive backup to GitHub backup (#870)

  GitHub backup can now optionally include spool inventory (with usage
  history) and print archive metadata as JSON. Both toggles are off by
  default. No binary files (gcode/3MF) are included.
maziggy 1 month ago
parent
commit
f4df4393be

+ 1 - 0
CHANGELOG.md

@@ -15,6 +15,7 @@ All notable changes to Bambuddy will be documented in this file.
 - **REST/Webhook Smart Plug Type** ([#472](https://github.com/maziggy/bambuddy/issues/472)) — New "REST" smart plug type for controlling power via generic HTTP APIs. Works with any home automation platform that has an HTTP endpoint (openHAB, ioBroker, FHEM, Node-RED, etc.). Configure separate ON/OFF URLs with custom HTTP methods (GET/POST/PUT/PATCH), request bodies, and headers. Optional status polling via a GET endpoint with JSON path extraction for state, power, and energy monitoring. Fully controllable — supports auto on/off with prints, daily scheduling, sidebar quick-toggle, and power alerts.
 - **Configurable Default Print Options** ([#858](https://github.com/maziggy/bambuddy/issues/858)) — Print options (bed levelling, flow calibration, vibration calibration, first layer inspection, timelapse) now have configurable defaults in Settings → Workflow. Set your preferred defaults once and every new print dialog starts with those values. Still overridable per print.
 - **Batch Print Quantity** ([#342](https://github.com/maziggy/bambuddy/issues/342)) — Print multiple copies of a file in one step. The print and schedule dialogs now have a quantity field — set it to any number and the system creates that many queue items automatically. When quantity is greater than one, items are grouped into a batch for tracking. In the direct print dialog, the first copy prints immediately while the remaining copies are queued. The queue page shows a batch badge on grouped items. Batch progress and cancellation are available via the API.
+- **GitHub Backup: Spool Inventory & Print Archives** ([#870](https://github.com/maziggy/bambuddy/issues/870)) — GitHub backup can now include spool inventory and print archive history as optional toggles alongside the existing K-profiles, cloud profiles, and settings. Spool backup exports all spools with their material, brand, color, weight, cost tracking, RFID tags, and full usage history. Archive backup exports print history metadata (filament, temperatures, times, costs, energy) — no gcode/3MF binary files. Both are off by default and can be enabled independently in Settings → Backup & Restore.
 
 ### Improved
 - **Queue Page Visual Refresh** — Compact stats bar replaces the five summary cards (saves vertical space), color-coded left borders on all queue items for instant status scanning, collapsible history section (collapsed by default), and condensed single-line rows for history items showing more prints at a glance.

+ 6 - 0
backend/app/api/routes/github_backup.py

@@ -39,6 +39,8 @@ def _config_to_response(config: GitHubBackupConfig) -> dict:
         "backup_kprofiles": config.backup_kprofiles,
         "backup_cloud_profiles": config.backup_cloud_profiles,
         "backup_settings": config.backup_settings,
+        "backup_spools": config.backup_spools,
+        "backup_archives": config.backup_archives,
         "enabled": config.enabled,
         "last_backup_at": config.last_backup_at,
         "last_backup_status": config.last_backup_status,
@@ -89,6 +91,8 @@ async def save_config(
         config.backup_kprofiles = config_data.backup_kprofiles
         config.backup_cloud_profiles = config_data.backup_cloud_profiles
         config.backup_settings = config_data.backup_settings
+        config.backup_spools = config_data.backup_spools
+        config.backup_archives = config_data.backup_archives
         config.enabled = config_data.enabled
 
         # Calculate next scheduled run if enabled
@@ -109,6 +113,8 @@ async def save_config(
             backup_kprofiles=config_data.backup_kprofiles,
             backup_cloud_profiles=config_data.backup_cloud_profiles,
             backup_settings=config_data.backup_settings,
+            backup_spools=config_data.backup_spools,
+            backup_archives=config_data.backup_archives,
             enabled=config_data.enabled,
         )
 

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

@@ -1618,6 +1618,16 @@ async def run_migrations(conn):
     except OperationalError:
         pass
 
+    # Migration: Add backup_spools and backup_archives columns to github_backup_config
+    try:
+        await conn.execute(text("ALTER TABLE github_backup_config ADD COLUMN backup_spools BOOLEAN DEFAULT 0"))
+    except OperationalError:
+        pass
+    try:
+        await conn.execute(text("ALTER TABLE github_backup_config ADD COLUMN backup_archives BOOLEAN DEFAULT 0"))
+    except OperationalError:
+        pass
+
     # Seed default settings keys that must exist on fresh install
     default_settings = [
         ("advanced_auth_enabled", "false"),

+ 2 - 0
backend/app/models/github_backup.py

@@ -27,6 +27,8 @@ class GitHubBackupConfig(Base):
     backup_kprofiles: Mapped[bool] = mapped_column(Boolean, default=True)
     backup_cloud_profiles: Mapped[bool] = mapped_column(Boolean, default=True)
     backup_settings: Mapped[bool] = mapped_column(Boolean, default=False)
+    backup_spools: Mapped[bool] = mapped_column(Boolean, default=False)
+    backup_archives: Mapped[bool] = mapped_column(Boolean, default=False)
 
     # Status tracking
     enabled: Mapped[bool] = mapped_column(Boolean, default=True)

+ 6 - 0
backend/app/schemas/github_backup.py

@@ -29,6 +29,8 @@ class GitHubBackupConfigCreate(BaseModel):
     backup_kprofiles: bool = Field(default=True, description="Backup K-profiles")
     backup_cloud_profiles: bool = Field(default=True, description="Backup Bambu Cloud profiles")
     backup_settings: bool = Field(default=False, description="Backup app settings")
+    backup_spools: bool = Field(default=False, description="Backup spool inventory")
+    backup_archives: bool = Field(default=False, description="Backup print archive history")
 
     enabled: bool = Field(default=True, description="Enable backup feature")
 
@@ -60,6 +62,8 @@ class GitHubBackupConfigUpdate(BaseModel):
     backup_kprofiles: bool | None = None
     backup_cloud_profiles: bool | None = None
     backup_settings: bool | None = None
+    backup_spools: bool | None = None
+    backup_archives: bool | None = None
 
     enabled: bool | None = None
 
@@ -92,6 +96,8 @@ class GitHubBackupConfigResponse(BaseModel):
     backup_kprofiles: bool
     backup_cloud_profiles: bool
     backup_settings: bool
+    backup_spools: bool
+    backup_archives: bool
 
     enabled: bool
     last_backup_at: datetime | None

+ 137 - 0
backend/app/services/github_backup.py

@@ -16,9 +16,12 @@ from sqlalchemy import desc, select
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from backend.app.core.database import async_session
+from backend.app.models.archive import PrintArchive
 from backend.app.models.github_backup import GitHubBackupConfig, GitHubBackupLog
 from backend.app.models.printer import Printer
 from backend.app.models.settings import Settings
+from backend.app.models.spool import Spool
+from backend.app.models.spool_usage_history import SpoolUsageHistory
 from backend.app.services.bambu_cloud import get_cloud_service
 from backend.app.services.printer_manager import printer_manager
 
@@ -331,6 +334,8 @@ class GitHubBackupService:
                 "kprofiles": config.backup_kprofiles,
                 "cloud_profiles": config.backup_cloud_profiles,
                 "settings": config.backup_settings,
+                "spools": config.backup_spools,
+                "archives": config.backup_archives,
             },
         }
         files["backup_metadata.json"] = metadata
@@ -350,6 +355,16 @@ class GitHubBackupService:
             self._backup_progress = "Collecting app settings..."
             await self._collect_settings(db, files)
 
+        # Collect spool inventory
+        if config.backup_spools:
+            self._backup_progress = "Collecting spool inventory..."
+            await self._collect_spools(db, files)
+
+        # Collect print archives
+        if config.backup_archives:
+            self._backup_progress = "Collecting print archives..."
+            await self._collect_archives(db, files)
+
         return files
 
     async def _collect_kprofiles(self, db: AsyncSession, files: dict):
@@ -472,6 +487,128 @@ class GitHubBackupService:
             "settings": settings_data,
         }
 
+    async def _collect_spools(self, db: AsyncSession, files: dict):
+        """Collect spool inventory data."""
+        result = await db.execute(select(Spool))
+        spools = result.scalars().all()
+
+        if not spools:
+            return
+
+        spool_list = []
+        for s in spools:
+            spool_data = {
+                "id": s.id,
+                "material": s.material,
+                "subtype": s.subtype,
+                "color_name": s.color_name,
+                "rgba": s.rgba,
+                "brand": s.brand,
+                "label_weight": s.label_weight,
+                "core_weight": s.core_weight,
+                "weight_used": s.weight_used,
+                "weight_locked": s.weight_locked,
+                "slicer_filament": s.slicer_filament,
+                "slicer_filament_name": s.slicer_filament_name,
+                "nozzle_temp_min": s.nozzle_temp_min,
+                "nozzle_temp_max": s.nozzle_temp_max,
+                "note": s.note,
+                "cost_per_kg": s.cost_per_kg,
+                "tag_uid": s.tag_uid,
+                "tray_uuid": s.tray_uuid,
+                "data_origin": s.data_origin,
+                "tag_type": s.tag_type,
+                "archived_at": str(s.archived_at) if s.archived_at else None,
+                "created_at": str(s.created_at) if s.created_at else None,
+            }
+            spool_list.append(spool_data)
+
+        files["spools/inventory.json"] = {
+            "version": "1.0",
+            "spools": spool_list,
+        }
+
+        # Collect usage history
+        usage_result = await db.execute(select(SpoolUsageHistory))
+        usages = usage_result.scalars().all()
+
+        if usages:
+            usage_list = []
+            for u in usages:
+                usage_list.append(
+                    {
+                        "id": u.id,
+                        "spool_id": u.spool_id,
+                        "printer_id": u.printer_id,
+                        "print_name": u.print_name,
+                        "archive_id": u.archive_id,
+                        "weight_used": u.weight_used,
+                        "percent_used": u.percent_used,
+                        "status": u.status,
+                        "cost": u.cost,
+                        "created_at": str(u.created_at) if u.created_at else None,
+                    }
+                )
+            files["spools/usage_history.json"] = {
+                "version": "1.0",
+                "usage_history": usage_list,
+            }
+
+        logger.info("Collected %d spools and %d usage records", len(spool_list), len(usages))
+
+    async def _collect_archives(self, db: AsyncSession, files: dict):
+        """Collect print archive metadata (no binary files)."""
+        result = await db.execute(select(PrintArchive))
+        archives = result.scalars().all()
+
+        if not archives:
+            return
+
+        archive_list = []
+        for a in archives:
+            archive_data = {
+                "id": a.id,
+                "printer_id": a.printer_id,
+                "project_id": a.project_id,
+                "filename": a.filename,
+                "file_size": a.file_size,
+                "content_hash": a.content_hash,
+                "print_name": a.print_name,
+                "print_time_seconds": a.print_time_seconds,
+                "filament_used_grams": a.filament_used_grams,
+                "filament_type": a.filament_type,
+                "filament_color": a.filament_color,
+                "layer_height": a.layer_height,
+                "total_layers": a.total_layers,
+                "nozzle_diameter": a.nozzle_diameter,
+                "bed_temperature": a.bed_temperature,
+                "nozzle_temperature": a.nozzle_temperature,
+                "sliced_for_model": a.sliced_for_model,
+                "status": a.status,
+                "started_at": str(a.started_at) if a.started_at else None,
+                "completed_at": str(a.completed_at) if a.completed_at else None,
+                "makerworld_url": a.makerworld_url,
+                "designer": a.designer,
+                "external_url": a.external_url,
+                "is_favorite": a.is_favorite,
+                "tags": a.tags,
+                "notes": a.notes,
+                "cost": a.cost,
+                "failure_reason": a.failure_reason,
+                "quantity": a.quantity,
+                "energy_kwh": a.energy_kwh,
+                "energy_cost": a.energy_cost,
+                "created_at": str(a.created_at) if a.created_at else None,
+            }
+            archive_list.append(archive_data)
+
+        files["archives/print_history.json"] = {
+            "version": "1.0",
+            "archives": archive_list,
+        }
+
+        logger.info("Collected %d print archives", len(archive_list))
+
     async def _push_to_github(self, config: GitHubBackupConfig, files: dict) -> dict:
         """Push files to GitHub using the GitHub API.
 

+ 64 - 0
backend/tests/integration/test_github_backup_api.py

@@ -28,6 +28,8 @@ class TestGitHubBackupConfigAPI:
             "backup_kprofiles": True,
             "backup_cloud_profiles": True,
             "backup_settings": False,
+            "backup_spools": False,
+            "backup_archives": False,
             "enabled": True,
         }
         response = await async_client.post("/api/v1/github-backup/config", json=data)
@@ -37,6 +39,8 @@ class TestGitHubBackupConfigAPI:
         assert result["branch"] == "main"
         assert result["has_token"] is True
         assert result["enabled"] is True
+        assert result["backup_spools"] is False
+        assert result["backup_archives"] is False
         # Token should not be exposed in response
         assert "access_token" not in result
 
@@ -67,6 +71,30 @@ class TestGitHubBackupConfigAPI:
         assert result["branch"] == "develop"
         assert result["schedule_type"] == "weekly"
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_create_config_with_spools_and_archives(self, async_client: AsyncClient):
+        """Verify config with spool and archive backup enabled."""
+        data = {
+            "repository_url": "https://github.com/test/spoolarchive",
+            "access_token": "ghp_spooltoken",
+            "branch": "main",
+            "schedule_enabled": False,
+            "schedule_type": "daily",
+            "backup_kprofiles": True,
+            "backup_cloud_profiles": False,
+            "backup_settings": False,
+            "backup_spools": True,
+            "backup_archives": True,
+            "enabled": True,
+        }
+        response = await async_client.post("/api/v1/github-backup/config", json=data)
+        assert response.status_code == 200
+        result = response.json()
+        assert result["backup_spools"] is True
+        assert result["backup_archives"] is True
+        assert result["backup_cloud_profiles"] is False
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_update_config_partial(self, async_client: AsyncClient):
@@ -81,6 +109,8 @@ class TestGitHubBackupConfigAPI:
             "backup_kprofiles": True,
             "backup_cloud_profiles": True,
             "backup_settings": False,
+            "backup_spools": False,
+            "backup_archives": False,
             "enabled": True,
         }
         await async_client.post("/api/v1/github-backup/config", json=create_data)
@@ -98,6 +128,40 @@ class TestGitHubBackupConfigAPI:
         # Original values should be preserved
         assert result["repository_url"] == "https://github.com/test/update"
 
+    @pytest.mark.asyncio
+    @pytest.mark.integration
+    async def test_update_config_enable_spools_and_archives(self, async_client: AsyncClient):
+        """Verify partial update can enable spool and archive backup."""
+        # Create config first
+        create_data = {
+            "repository_url": "https://github.com/test/updatetoggle",
+            "access_token": "ghp_toggletoken",
+            "branch": "main",
+            "schedule_enabled": False,
+            "schedule_type": "daily",
+            "backup_kprofiles": True,
+            "backup_cloud_profiles": True,
+            "backup_settings": False,
+            "backup_spools": False,
+            "backup_archives": False,
+            "enabled": True,
+        }
+        await async_client.post("/api/v1/github-backup/config", json=create_data)
+
+        # Enable spools and archives via partial update
+        update_data = {
+            "backup_spools": True,
+            "backup_archives": True,
+        }
+        response = await async_client.patch("/api/v1/github-backup/config", json=update_data)
+        assert response.status_code == 200
+        result = response.json()
+        assert result["backup_spools"] is True
+        assert result["backup_archives"] is True
+        # Other values preserved
+        assert result["backup_kprofiles"] is True
+        assert result["backup_settings"] is False
+
     @pytest.mark.asyncio
     @pytest.mark.integration
     async def test_delete_config(self, async_client: AsyncClient):

+ 34 - 0
frontend/src/__tests__/api/githubBackupApi.test.ts

@@ -33,6 +33,8 @@ describe('GitHub Backup API Types', () => {
       backup_kprofiles: true,
       backup_cloud_profiles: true,
       backup_settings: false,
+      backup_spools: false,
+      backup_archives: false,
       enabled: true,
       last_backup_at: '2026-01-27T10:00:00Z',
       last_backup_status: 'success',
@@ -46,6 +48,8 @@ describe('GitHub Backup API Types', () => {
     expect(config.id).toBe(1);
     expect(config.has_token).toBe(true);
     expect(config.schedule_type).toBe('daily');
+    expect(config.backup_spools).toBe(false);
+    expect(config.backup_archives).toBe(false);
   });
 
   it('GitHubBackupStatus has correct shape', () => {
@@ -96,6 +100,34 @@ describe('GitHub Backup API Types', () => {
     expect(log.files_changed).toBe(5);
   });
 
+  it('GitHubBackupConfig supports spool and archive backup toggles', () => {
+    const config: GitHubBackupConfig = {
+      id: 2,
+      repository_url: 'https://github.com/test/full-backup',
+      has_token: true,
+      branch: 'main',
+      schedule_enabled: false,
+      schedule_type: 'daily',
+      backup_kprofiles: true,
+      backup_cloud_profiles: false,
+      backup_settings: false,
+      backup_spools: true,
+      backup_archives: true,
+      enabled: true,
+      last_backup_at: null,
+      last_backup_status: null,
+      last_backup_message: null,
+      last_backup_commit_sha: null,
+      next_scheduled_run: null,
+      created_at: '2026-04-01T00:00:00Z',
+      updated_at: '2026-04-01T00:00:00Z',
+    };
+
+    expect(config.backup_spools).toBe(true);
+    expect(config.backup_archives).toBe(true);
+    expect(config.backup_cloud_profiles).toBe(false);
+  });
+
   it('GitHubBackupLog can have error', () => {
     const log: GitHubBackupLog = {
       id: 2,
@@ -139,6 +171,8 @@ describe('GitHub Backup API Endpoints', () => {
       backup_kprofiles: true,
       backup_cloud_profiles: true,
       backup_settings: false,
+      backup_spools: true,
+      backup_archives: true,
       enabled: true,
       last_backup_at: null,
       last_backup_status: null,

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

@@ -1715,6 +1715,8 @@ export interface GitHubBackupConfig {
   backup_kprofiles: boolean;
   backup_cloud_profiles: boolean;
   backup_settings: boolean;
+  backup_spools: boolean;
+  backup_archives: boolean;
   enabled: boolean;
   last_backup_at: string | null;
   last_backup_status: string | null;
@@ -1734,6 +1736,8 @@ export interface GitHubBackupConfigCreate {
   backup_kprofiles?: boolean;
   backup_cloud_profiles?: boolean;
   backup_settings?: boolean;
+  backup_spools?: boolean;
+  backup_archives?: boolean;
   enabled?: boolean;
 }
 

+ 36 - 2
frontend/src/components/GitHubBackupSettings.tsx

@@ -87,6 +87,8 @@ export function GitHubBackupSettings() {
   const [backupKProfiles, setBackupKProfiles] = useState(true);
   const [backupCloudProfiles, setBackupCloudProfiles] = useState(true);
   const [backupSettings, setBackupSettings] = useState(false);
+  const [backupSpools, setBackupSpools] = useState(false);
+  const [backupArchives, setBackupArchives] = useState(false);
   const [enabled, setEnabled] = useState(true);
 
   // Local backup state
@@ -180,6 +182,8 @@ export function GitHubBackupSettings() {
       setBackupKProfiles(config.backup_kprofiles);
       setBackupCloudProfiles(config.backup_cloud_profiles);
       setBackupSettings(config.backup_settings);
+      setBackupSpools(config.backup_spools);
+      setBackupArchives(config.backup_archives);
       setEnabled(config.enabled);
       setAccessToken(''); // Don't show stored token
       // Mark as initialized after a tick to avoid auto-save on initial load
@@ -203,6 +207,8 @@ export function GitHubBackupSettings() {
           backup_kprofiles: backupKProfiles,
           backup_cloud_profiles: backupCloudProfiles,
           backup_settings: backupSettings,
+          backup_spools: backupSpools,
+          backup_archives: backupArchives,
           enabled,
         });
         setAccessToken(''); // Clear after save
@@ -217,6 +223,8 @@ export function GitHubBackupSettings() {
           backup_kprofiles: backupKProfiles,
           backup_cloud_profiles: backupCloudProfiles,
           backup_settings: backupSettings,
+          backup_spools: backupSpools,
+          backup_archives: backupArchives,
           enabled,
         });
         showToast(t('backup.settingsSaved'));
@@ -226,7 +234,7 @@ export function GitHubBackupSettings() {
     } catch (error) {
       showToast(t('backup.failedToSave', { message: (error as Error).message }), 'error');
     }
-  }, [config?.has_token, repoUrl, accessToken, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, enabled, queryClient, showToast, t]);
+  }, [config?.has_token, repoUrl, accessToken, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, backupSpools, backupArchives, enabled, queryClient, showToast, t]);
 
   // Auto-save effect for existing configs (debounced)
   useEffect(() => {
@@ -245,7 +253,7 @@ export function GitHubBackupSettings() {
         clearTimeout(autoSaveTimerRef.current);
       }
     };
-  }, [repoUrl, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, enabled, autoSave, config?.has_token]);
+  }, [repoUrl, branch, scheduleEnabled, scheduleType, backupKProfiles, backupCloudProfiles, backupSettings, backupSpools, backupArchives, enabled, autoSave, config?.has_token]);
 
   // Auto-save token when it changes (with longer debounce)
   useEffect(() => {
@@ -361,6 +369,8 @@ export function GitHubBackupSettings() {
       backup_kprofiles: backupKProfiles,
       backup_cloud_profiles: backupCloudProfiles,
       backup_settings: backupSettings,
+      backup_spools: backupSpools,
+      backup_archives: backupArchives,
       enabled,
     });
   };
@@ -529,6 +539,30 @@ export function GitHubBackupSettings() {
                     <p className="text-xs text-bambu-gray">{t('backup.appSettingsDescription')}</p>
                   </div>
                 </label>
+                <label className="flex items-start gap-2 cursor-pointer">
+                  <input
+                    type="checkbox"
+                    checked={backupSpools}
+                    onChange={(e) => setBackupSpools(e.target.checked)}
+                    className="w-4 h-4 mt-0.5 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
+                  />
+                  <div>
+                    <span className="text-white text-sm">{t('backup.spoolInventory')}</span>
+                    <p className="text-xs text-bambu-gray">{t('backup.spoolInventoryDescription')}</p>
+                  </div>
+                </label>
+                <label className="flex items-start gap-2 cursor-pointer">
+                  <input
+                    type="checkbox"
+                    checked={backupArchives}
+                    onChange={(e) => setBackupArchives(e.target.checked)}
+                    className="w-4 h-4 mt-0.5 rounded border-bambu-dark-tertiary bg-bambu-dark text-bambu-green focus:ring-bambu-green"
+                  />
+                  <div>
+                    <span className="text-white text-sm">{t('backup.printArchives')}</span>
+                    <p className="text-xs text-bambu-gray">{t('backup.printArchivesDescription')}</p>
+                  </div>
+                </label>
               </div>
             </div>
 

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

@@ -3261,6 +3261,10 @@ export default {
     cloudProfilesDescription: 'Filament-, Drucker- und Prozessprofile aus der Bambu Cloud',
     appSettings: 'App-Einstellungen',
     appSettingsDescription: 'Bambuddy-Konfiguration (komplette Datenbank)',
+    spoolInventory: 'Spulenbestand',
+    spoolInventoryDescription: 'Filamentspulen, Nutzungsverlauf und Kostenverfolgung',
+    printArchives: 'Druckarchive',
+    printArchivesDescription: 'Druckverlauf-Metadaten (keine GCode/3MF-Dateien)',
     lastBackupAt: 'Letzte Sicherung:',
     noBackupsYet: 'Noch keine Sicherungen',
     next: 'Nächste:',

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

@@ -3266,6 +3266,10 @@ export default {
     cloudProfilesDescription: 'Filament, printer, and process presets from Bambu Cloud',
     appSettings: 'App Settings',
     appSettingsDescription: 'Bambuddy configuration (complete database)',
+    spoolInventory: 'Spool Inventory',
+    spoolInventoryDescription: 'Filament spools, usage history, and cost tracking',
+    printArchives: 'Print Archives',
+    printArchivesDescription: 'Print history metadata (no gcode/3MF files)',
     lastBackupAt: 'Last backup:',
     noBackupsYet: 'No backups yet',
     next: 'Next:',

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

@@ -3252,6 +3252,10 @@ export default {
     cloudProfilesDescription: 'Préréglages de filament, imprimante et processus depuis Bambu Cloud',
     appSettings: 'Paramètres de l\'application',
     appSettingsDescription: 'Configuration Bambuddy (base de données complète)',
+    spoolInventory: 'Inventaire des bobines',
+    spoolInventoryDescription: 'Bobines de filament, historique d\'utilisation et suivi des coûts',
+    printArchives: 'Archives d\'impression',
+    printArchivesDescription: 'Métadonnées de l\'historique d\'impression (pas de fichiers gcode/3MF)',
     lastBackupAt: 'Dernière sauvegarde :',
     noBackupsYet: 'Aucune sauvegarde pour l\'instant',
     next: 'Prochaine :',

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

@@ -3251,6 +3251,10 @@ export default {
     cloudProfilesDescription: 'Preset di filamento, stampante e processo da Bambu Cloud',
     appSettings: 'Impostazioni App',
     appSettingsDescription: 'Configurazione Bambuddy (database completo)',
+    spoolInventory: 'Inventario bobine',
+    spoolInventoryDescription: 'Bobine di filamento, cronologia utilizzo e tracciamento costi',
+    printArchives: 'Archivi di stampa',
+    printArchivesDescription: 'Metadati della cronologia di stampa (nessun file gcode/3MF)',
     lastBackupAt: 'Ultimo backup:',
     noBackupsYet: 'Nessun backup ancora',
     next: 'Prossimo:',

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

@@ -3264,6 +3264,10 @@ export default {
     cloudProfilesDescription: 'Bambu Cloudからのフィラメント、プリンター、プロセスプリセット',
     appSettings: 'アプリ設定',
     appSettingsDescription: 'Bambuddy設定(データベース全体)',
+    spoolInventory: 'スプール在庫',
+    spoolInventoryDescription: 'フィラメントスプール、使用履歴、コスト追跡',
+    printArchives: '印刷アーカイブ',
+    printArchivesDescription: '印刷履歴メタデータ(gcode/3MFファイルなし)',
     lastBackupAt: '最終バックアップ:',
     noBackupsYet: 'バックアップはまだありません',
     next: '次回:',

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

@@ -3251,6 +3251,10 @@ export default {
     cloudProfilesDescription: 'Predefinições de filamento, impressora e processo do Bambu Cloud',
     appSettings: 'Configurações do App',
     appSettingsDescription: 'Configuração do Bambuddy (banco de dados completo)',
+    spoolInventory: 'Inventário de bobinas',
+    spoolInventoryDescription: 'Bobinas de filamento, histórico de uso e rastreamento de custos',
+    printArchives: 'Arquivos de impressão',
+    printArchivesDescription: 'Metadados do histórico de impressão (sem arquivos gcode/3MF)',
     lastBackupAt: 'Último backup:',
     noBackupsYet: 'Nenhum backup ainda',
     next: 'Próximo:',

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

@@ -3251,6 +3251,10 @@ export default {
     cloudProfilesDescription: '来自 Bambu Cloud 的耗材、打印机和工艺预设',
     appSettings: '应用设置',
     appSettingsDescription: 'Bambuddy 配置(完整数据库)',
+    spoolInventory: '耗材库存',
+    spoolInventoryDescription: '耗材卷轴、使用记录和成本追踪',
+    printArchives: '打印档案',
+    printArchivesDescription: '打印历史元数据(不含 gcode/3MF 文件)',
     lastBackupAt: '上次备份:',
     noBackupsYet: '尚无备份',
     next: '下次:',

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BdvAN3ky.js


+ 1 - 1
static/index.html

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

Some files were not shown because too many files changed in this diff