diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1354c1ef3..04d2cf559 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,8 +6,8 @@ on: pull_request: # Run on all PRs, but skip for repo owner (runs local tests) -# Skip CI for repo owner's PRs (they run tests locally) -# This check is applied to all jobs below +# Skip CI for PRs authored by repo owner (they run tests locally) +# Uses PR author instead of triggering actor so rebasing by owner doesn't skip CI env: PYTHON_VERSION: '3.11' @@ -30,7 +30,7 @@ jobs: backend-lint: name: Backend Lint runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner steps: - uses: actions/checkout@v4 @@ -51,7 +51,7 @@ jobs: backend-security: name: Backend Security runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner continue-on-error: true steps: - uses: actions/checkout@v4 @@ -73,7 +73,7 @@ jobs: backend-tests: name: Backend Tests runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner needs: backend-lint steps: - uses: actions/checkout@v4 @@ -110,7 +110,7 @@ jobs: frontend-lint: name: Frontend Lint runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner steps: - uses: actions/checkout@v4 @@ -132,7 +132,7 @@ jobs: frontend-security: name: Frontend Security runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner continue-on-error: true steps: - uses: actions/checkout@v4 @@ -155,7 +155,7 @@ jobs: frontend-typecheck: name: Frontend Type Check runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner steps: - uses: actions/checkout@v4 @@ -177,7 +177,7 @@ jobs: frontend-tests: name: Frontend Tests runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner needs: [frontend-lint, frontend-typecheck] steps: - uses: actions/checkout@v4 @@ -201,7 +201,7 @@ jobs: frontend-build: name: Frontend Build runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner needs: [frontend-tests] steps: - uses: actions/checkout@v4 @@ -228,7 +228,7 @@ jobs: docker-test: name: Docker Build runs-on: ubuntu-latest - if: github.event_name == 'push' || github.actor != github.repository_owner + if: github.event_name == 'push' || github.event.pull_request.user.login != github.repository_owner timeout-minutes: 20 needs: [backend-tests, frontend-build] steps: diff --git a/.github/workflows/issue-closed.yml b/.github/workflows/issue-closed.yml new file mode 100644 index 000000000..31f2e607b --- /dev/null +++ b/.github/workflows/issue-closed.yml @@ -0,0 +1,37 @@ +name: Clean up closed issues + +on: + issues: + types: [closed] + +permissions: + issues: write + +jobs: + remove-labels: + runs-on: ubuntu-latest + steps: + - name: Remove feedback label + uses: actions/github-script@v7 + with: + script: | + const issue = context.payload.issue; + const hasLabel = issue.labels.some(l => l.name === 'feedback'); + + if (hasLabel) { + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue.number, + name: 'feedback' + }); + console.log(`Removed 'feedback' label from issue #${issue.number}`); + } catch (error) { + if (error.status === 404) { + console.log(`Label 'feedback' already removed from issue #${issue.number}`); + } else { + throw error; + } + } + } diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 4ada6255e..03e2b7824 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -18,4 +18,3 @@ jobs: days-before-stale: 21 days-before-close: 7 stale-issue-label: 'stale' - remove-issue-labels: 'feedback' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7beef5866..65921562a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,17 @@ All notable changes to Bambuddy will be documented in this file. -## [0.1.6] - Not released +## [0.1.6-final] - Not released ### New Features +- **Disable Printer Firmware Checks** - New toggle in Settings → General → Updates to disable printer firmware update checks: + - Prevents Bambuddy from checking Bambu Lab servers for firmware updates + - Useful for users who prefer to manage firmware manually or have network restrictions +- **Archive Plate Browsing** - Browse plate thumbnails directly in archive cards (Issue #166): + - Hover over archive card to reveal plate navigation for multi-plate files + - Left/right arrows to cycle through plate thumbnails + - Dot indicators show current plate (clickable to jump to specific plate) + - Lazy-loads plate data only when user hovers - **GitHub Profile Backup** - Automatically backup your Cloud profiles, K-profiles and settings to a GitHub repository: - Configure GitHub repository URL and Personal Access Token - Schedule backups hourly, daily, or weekly @@ -102,6 +110,13 @@ All notable changes to Bambuddy will be documented in this file. - New "Print Queue" section in notification provider settings ### Fixes +- **Multi-Plate Thumbnail in Queue** - Fixed queue items showing wrong thumbnail for multi-plate files (Issue #166): + - Queue now displays the correct plate thumbnail based on selected plate + - Previously always showed plate 1 thumbnail regardless of selection +- **A1/A1 Mini Shows Printing Instead of Idle** - Fixed incorrect status display for A1 series printers (Issue #168): + - Some A1/A1 Mini firmware versions incorrectly report stage 0 ("Printing") when idle + - Now checks gcode_state to correctly display "Idle" for affected printers + - Fix only applies to A1 models with the specific buggy condition - **HMS Error Notifications** - Get notified when printer errors occur (Issue #84): - Automatic notifications for HMS errors (AMS issues, nozzle problems, etc.) - Human-readable error messages (853 error codes translated) @@ -132,6 +147,11 @@ All notable changes to Bambuddy will be documented in this file. - Text wrap toggle: "Wrap" button in header to wrap long names instead of truncating - Both settings persist in localStorage - Tooltip shows full name on hover +- **K-Profiles Backup Status** - Fixed GitHub backup settings showing incorrect printer connection count (e.g., "1/2 connected" when both printers are connected); now fetches status from API instead of relying on WebSocket cache +- **GitHub Backup Timestamps** - Removed volatile timestamps from GitHub backup files so git diffs only show actual data changes + +### Maintenance +- Upgraded vitest from 2.x to 3.x to resolve npm audit security vulnerabilities in dev dependencies ## [0.1.6b11] - 2026-01-22 diff --git a/README.md b/README.md index 230b20908..18b027125 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ - Photo attachments & failure analysis - Timelapse editor (trim, speed, music) - Re-print to any connected printer with AMS mapping (auto-match or manual slot selection, multi-plate support) +- Plate thumbnail browsing for multi-plate archives (hover to navigate between plates) - Archive comparison (side-by-side diff) ### 📊 Monitoring & Control diff --git a/backend/.coverage b/backend/.coverage deleted file mode 100644 index 811e3bd30..000000000 Binary files a/backend/.coverage and /dev/null differ diff --git a/backend/app/api/routes/cloud.py b/backend/app/api/routes/cloud.py index 36ebf38d3..f1dc73cc5 100644 --- a/backend/app/api/routes/cloud.py +++ b/backend/app/api/routes/cloud.py @@ -267,6 +267,34 @@ _filament_cache_time: float = 0 FILAMENT_CACHE_TTL = 300 # 5 minutes +def _filament_id_to_setting_id(filament_id: str) -> str: + """ + Convert filament_id to setting_id format for Bambu Cloud API. + + Printers report filament_id (e.g., GFA00, GFG02) but the API expects + setting_id format which has an "S" inserted after "GF" (e.g., GFSA00, GFSG02). + + User presets (starting with "P") and already-correct IDs are returned unchanged. + """ + if not filament_id: + return filament_id + + # User presets start with "P" - leave unchanged + if filament_id.startswith("P"): + return filament_id + + # Official Bambu presets: GFx## -> GFSx## + # Check if it matches the filament_id pattern (GF followed by letter and digits) + if filament_id.startswith("GF") and len(filament_id) >= 4: + # Check if it's already a setting_id (has S after GF) + if filament_id[2] == "S": + return filament_id + # Insert "S" after "GF": GFA00 -> GFSA00 + return f"GFS{filament_id[2:]}" + + return filament_id + + @router.post("/filament-info") async def get_filament_info(setting_ids: list[str] = Body(...), db: AsyncSession = Depends(get_db)): """ @@ -308,7 +336,10 @@ async def get_filament_info(setting_ids: list[str] = Body(...), db: AsyncSession continue try: - data = await cloud.get_setting_detail(setting_id) + # Transform filament_id to setting_id format (GFA00 -> GFSA00) + api_setting_id = _filament_id_to_setting_id(setting_id) + + data = await cloud.get_setting_detail(api_setting_id) setting = data.get("setting", {}) # Extract name (e.g., "Bambu PLA Basic Jade White") @@ -323,11 +354,14 @@ async def get_filament_info(setting_ids: list[str] = Body(...), db: AsyncSession k_value = None info = {"name": name, "k": k_value} + # Cache using original ID so frontend gets expected response _filament_cache[setting_id] = info result[setting_id] = info except Exception as e: - logger.warning(f"Failed to get cloud preset {setting_id}: {e}") + logger.warning( + f"Failed to get cloud preset {setting_id} (API ID: {_filament_id_to_setting_id(setting_id)}): {e}" + ) # Cache the failure to avoid repeated requests _filament_cache[setting_id] = {"name": "", "k": None} result[setting_id] = {"name": "", "k": None} diff --git a/backend/app/api/routes/printers.py b/backend/app/api/routes/printers.py index 6e8f86a92..811272c66 100644 --- a/backend/app/api/routes/printers.py +++ b/backend/app/api/routes/printers.py @@ -408,7 +408,7 @@ async def get_printer_status(printer_id: int, db: AsyncSession = Depends(get_db) nozzles=nozzles, print_options=print_options, stg_cur=state.stg_cur, - stg_cur_name=get_derived_status_name(state), + stg_cur_name=get_derived_status_name(state, printer.model), stg=state.stg, airduct_mode=state.airduct_mode, speed_level=state.speed_level, diff --git a/backend/app/api/routes/settings.py b/backend/app/api/routes/settings.py index 7ea8e7061..b687d3ff9 100644 --- a/backend/app/api/routes/settings.py +++ b/backend/app/api/routes/settings.py @@ -74,6 +74,7 @@ async def get_settings(db: AsyncSession = Depends(get_db)): "capture_finish_photo", "spoolman_enabled", "check_updates", + "check_printer_firmware", "virtual_printer_enabled", "ftp_retry_enabled", "mqtt_enabled", diff --git a/backend/app/schemas/settings.py b/backend/app/schemas/settings.py index 0e41fa820..75de9a310 100644 --- a/backend/app/schemas/settings.py +++ b/backend/app/schemas/settings.py @@ -26,6 +26,7 @@ class AppSettings(BaseModel): # Updates check_updates: bool = Field(default=True, description="Automatically check for updates on startup") + check_printer_firmware: bool = Field(default=True, description="Check for printer firmware updates from Bambu Lab") # Language notification_language: str = Field(default="en", description="Language for push notifications (en, de)") @@ -134,6 +135,7 @@ class AppSettingsUpdate(BaseModel): spoolman_url: str | None = None spoolman_sync_mode: str | None = None check_updates: bool | None = None + check_printer_firmware: bool | None = None notification_language: str | None = None ams_humidity_good: int | None = None ams_humidity_fair: int | None = None diff --git a/backend/app/services/github_backup.py b/backend/app/services/github_backup.py index e7e1bfb1f..d499948a7 100644 --- a/backend/app/services/github_backup.py +++ b/backend/app/services/github_backup.py @@ -309,13 +309,11 @@ class GitHubBackupService: } """ files: dict[str, dict | list] = {} - now = datetime.now(UTC) - # Metadata file + # Metadata file (no timestamps - git tracks file history) metadata = { "version": "1.0", "backup_type": "bambuddy_profiles", - "created_at": now.isoformat(), "contents": { "kprofiles": config.backup_kprofiles, "cloud_profiles": config.backup_cloud_profiles, @@ -365,7 +363,6 @@ class GitHubBackupService: "printer_name": printer.name, "printer_serial": serial, "nozzle_diameter": nozzle, - "exported_at": datetime.now(UTC).isoformat(), "profiles": [ { "slot_id": p.slot_id, @@ -425,21 +422,18 @@ class GitHubBackupService: if filament_settings: files["cloud_profiles/filament.json"] = { "version": "1.0", - "exported_at": datetime.now(UTC).isoformat(), "profiles": filament_settings, } if printer_settings: files["cloud_profiles/printer.json"] = { "version": "1.0", - "exported_at": datetime.now(UTC).isoformat(), "profiles": printer_settings, } if process_settings: files["cloud_profiles/process.json"] = { "version": "1.0", - "exported_at": datetime.now(UTC).isoformat(), "profiles": process_settings, } @@ -462,7 +456,6 @@ class GitHubBackupService: files["settings/app_settings.json"] = { "version": "1.0", - "exported_at": datetime.now(UTC).isoformat(), "settings": settings_data, } diff --git a/backend/app/services/printer_manager.py b/backend/app/services/printer_manager.py index ace0e8106..4457b5fb4 100644 --- a/backend/app/services/printer_manager.py +++ b/backend/app/services/printer_manager.py @@ -33,6 +33,22 @@ CHAMBER_TEMP_SUPPORTED_MODELS = frozenset( ] ) +# Models that may incorrectly report stg_cur=0 when idle (firmware bug) +# Based on Home Assistant Bambu Lab integration observations +# See: https://github.com/greghesp/ha-bambulab/blob/main/custom_components/bambu_lab/pybambu/models.py +A1_MODELS = frozenset( + [ + # Display names + "A1", + "A1 MINI", + "A1-MINI", + "A1MINI", + # Internal codes (from MQTT/SSDP) + "N1", # A1 Mini + "N2S", # A1 + ] +) + def supports_chamber_temp(model: str | None) -> bool: """Check if a printer model has a real chamber temperature sensor. @@ -47,6 +63,19 @@ def supports_chamber_temp(model: str | None) -> bool: return model_upper in CHAMBER_TEMP_SUPPORTED_MODELS +def has_stg_cur_idle_bug(model: str | None) -> bool: + """Check if a printer model may incorrectly report stg_cur=0 when idle. + + Some A1/A1 Mini firmware versions report stg_cur=0 (which maps to "Printing") + even when the printer is idle. This is a known firmware bug that was observed + in the Home Assistant Bambu Lab integration. + """ + if not model: + return False + model_upper = model.strip().upper() + return model_upper in A1_MODELS + + class PrinterInfo: """Basic printer info for callbacks.""" @@ -373,13 +402,22 @@ class PrinterManager: return result -def get_derived_status_name(state: PrinterState) -> str | None: +def get_derived_status_name(state: PrinterState, model: str | None = None) -> str | None: """ Compute a human-readable status name based on printer state. Uses stg_cur when available, otherwise derives status from temperature data when the printer is heating before a print starts. + + Args: + state: The printer state to analyze + model: Optional printer model for model-specific workarounds """ + # A1/A1 Mini firmware bug: some versions report stg_cur=0 when idle + # Only correct this specific case (IDLE + stg_cur=0) for affected models + if state.state == "IDLE" and state.stg_cur == 0 and has_stg_cur_idle_bug(model): + return None + # If we have a valid calibration stage, use it # X1 models use -1 for idle, A1/P1 models use 255 for idle # Valid stage numbers are 0-254 @@ -581,7 +619,7 @@ def printer_state_to_dict(state: PrinterState, printer_id: int | None = None, mo "wifi_signal": state.wifi_signal, # Calibration stage tracking "stg_cur": state.stg_cur, - "stg_cur_name": get_derived_status_name(state), + "stg_cur_name": get_derived_status_name(state, model), # Printable objects count for skip objects feature "printable_objects_count": len(state.printable_objects), # Fan speeds (0-100 percentage, None if not available) diff --git a/backend/app/services/spoolman.py b/backend/app/services/spoolman.py index 3be407df1..bf95ae8ca 100644 --- a/backend/app/services/spoolman.py +++ b/backend/app/services/spoolman.py @@ -541,10 +541,15 @@ class SpoolmanClient: # Need valid color to create filament tray_color = tray_data.get("tray_color", "") - if not tray_color or tray_color in ("", "00000000"): - logger.debug(f"Skipping tray with invalid color: {tray_color}") + if not tray_color or tray_color.strip() == "": + logger.debug("Skipping tray with empty color") return None + # Handle transparent/natural filament (RRGGBBAA with alpha=00) + # Replace with cream color that represents how natural PLA actually looks + if tray_color == "00000000": + tray_color = "F5E6D3FF" # Light cream/natural color + # Get sub_brands, falling back to tray_type tray_sub_brands = tray_data.get("tray_sub_brands", "") if not tray_sub_brands or tray_sub_brands.strip() == "": diff --git a/backend/tests/integration/test_settings_api.py b/backend/tests/integration/test_settings_api.py index 243d3cc1e..01c69efad 100644 --- a/backend/tests/integration/test_settings_api.py +++ b/backend/tests/integration/test_settings_api.py @@ -215,6 +215,28 @@ class TestSettingsAPI: assert result["currency"] == "JPY" assert result["check_updates"] is False + @pytest.mark.asyncio + @pytest.mark.integration + async def test_update_check_printer_firmware(self, async_client: AsyncClient): + """Verify check_printer_firmware can be updated.""" + # Default should be True + response = await async_client.get("/api/v1/settings/") + assert response.json()["check_printer_firmware"] is True + + # Update to False + response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": False}) + assert response.status_code == 200 + assert response.json()["check_printer_firmware"] is False + + # Verify persistence + response = await async_client.get("/api/v1/settings/") + assert response.json()["check_printer_firmware"] is False + + # Update back to True + response = await async_client.put("/api/v1/settings/", json={"check_printer_firmware": True}) + assert response.status_code == 200 + assert response.json()["check_printer_firmware"] is True + # ======================================================================== # MQTT settings tests # ======================================================================== diff --git a/backend/tests/unit/services/test_printer_manager.py b/backend/tests/unit/services/test_printer_manager.py index dbff3358e..25547f08b 100644 --- a/backend/tests/unit/services/test_printer_manager.py +++ b/backend/tests/unit/services/test_printer_manager.py @@ -12,6 +12,7 @@ import pytest from backend.app.services.printer_manager import ( PrinterManager, get_derived_status_name, + has_stg_cur_idle_bug, init_printer_connections, printer_state_to_dict, supports_chamber_temp, @@ -901,7 +902,7 @@ class TestGetDerivedStatusName: assert result == "Auto bed leveling" def test_stg_cur_zero_returns_printing(self): - """Verify stg_cur=0 returns 'Printing'.""" + """Verify stg_cur=0 returns 'Printing' when no model specified.""" state = MagicMock() state.stg_cur = 0 @@ -909,6 +910,72 @@ class TestGetDerivedStatusName: assert result == "Printing" + def test_a1_idle_with_stg_cur_zero_returns_none(self): + """Verify A1 with IDLE state and stg_cur=0 returns None (bug workaround).""" + state = MagicMock() + state.stg_cur = 0 + state.state = "IDLE" + + # Test various A1 model names + for model in ["A1", "A1 Mini", "A1-Mini", "A1MINI", "N1", "N2S"]: + result = get_derived_status_name(state, model) + assert result is None, f"Expected None for model {model}" + + def test_a1_running_with_stg_cur_zero_returns_printing(self): + """Verify A1 with RUNNING state and stg_cur=0 still returns 'Printing'.""" + state = MagicMock() + state.stg_cur = 0 + state.state = "RUNNING" + + result = get_derived_status_name(state, "A1") + + assert result == "Printing" + + def test_non_a1_idle_with_stg_cur_zero_returns_printing(self): + """Verify non-A1 models with IDLE and stg_cur=0 still return 'Printing'.""" + state = MagicMock() + state.stg_cur = 0 + state.state = "IDLE" + + # X1C should not get the workaround + result = get_derived_status_name(state, "X1C") + + assert result == "Printing" + + +class TestHasStgCurIdleBug: + """Tests for has_stg_cur_idle_bug function.""" + + def test_a1_models_return_true(self): + """Verify A1 model variants return True.""" + assert has_stg_cur_idle_bug("A1") is True + assert has_stg_cur_idle_bug("A1 Mini") is True + assert has_stg_cur_idle_bug("A1-Mini") is True + assert has_stg_cur_idle_bug("A1MINI") is True + assert has_stg_cur_idle_bug("a1") is True # case insensitive + assert has_stg_cur_idle_bug("a1 mini") is True + + def test_a1_internal_codes_return_true(self): + """Verify A1 internal model codes return True.""" + assert has_stg_cur_idle_bug("N1") is True # A1 Mini + assert has_stg_cur_idle_bug("N2S") is True # A1 + + def test_non_a1_models_return_false(self): + """Verify non-A1 models return False.""" + assert has_stg_cur_idle_bug("X1C") is False + assert has_stg_cur_idle_bug("X1") is False + assert has_stg_cur_idle_bug("P1P") is False + assert has_stg_cur_idle_bug("P1S") is False + assert has_stg_cur_idle_bug("H2D") is False + + def test_none_model_returns_false(self): + """Verify None model returns False.""" + assert has_stg_cur_idle_bug(None) is False + + def test_empty_model_returns_false(self): + """Verify empty model returns False.""" + assert has_stg_cur_idle_bug("") is False + class TestInitPrinterConnections: """Tests for init_printer_connections function.""" diff --git a/bambuddy-issue-notes.txt b/bambuddy-issue-notes.txt deleted file mode 100644 index 71c70f4c0..000000000 --- a/bambuddy-issue-notes.txt +++ /dev/null @@ -1,83 +0,0 @@ -=== BAMBUDDY FILE DELETION ISSUE - Jan 8, 2026 === -=== ROOT CAUSE IDENTIFIED === - -WHAT HAPPENED: -- /opt was COMPLETELY DELETED on TWO containers: - - Container 109 (claude): ~11:22 and ~12:22 - - Container 107 (3dp): ~13:28 -- Container 107 was "untouched" (no SSH, no Claude Code) - just running BamBuddy - -ROOT CAUSE FOUND: -Bug in backend/app/services/archive.py delete_archive() function (lines 914-929): - - file_path = settings.base_dir / archive.file_path - if file_path.exists(): - archive_dir = file_path.parent - shutil.rmtree(archive_dir, ignore_errors=True) # <-- THE BUG - -If archive.file_path is EMPTY or MALFORMED: -- file_path = /opt/bambuddy / "" = /opt/bambuddy -- archive_dir = file_path.parent = /opt -- shutil.rmtree("/opt") --> DELETES ENTIRE /opt DIRECTORY! - -TRIGGER: -- User was deleting archives via BamBuddy web UI on container 107 (3dp) -- One archive had corrupted/empty file_path in database -- Deleting that archive triggered shutil.rmtree("/opt") -- This deleted the entire /opt directory including BamBuddy itself - -TIMELINE FOR CONTAINER 107 (3dp): -- 13:28:19 - Normal operation (WebSocket disconnect) -- 13:28:44 - DELETE /api/v1/archives/* requests failing with 500 - (database already gone because /opt was deleted) -- ls -la / shows root directory modified at 13:28 - -FIX APPLIED (on container 109): -Safety checks added to delete_archive() in archive.py: -1. Check if file_path is not empty -2. Verify archive_dir is inside settings.archive_dir -3. Ensure archive_dir is at least 2 levels deep -4. Log error and refuse to delete if checks fail - -TO INVESTIGATE AFTER ROLLBACK: -On container 107, after rolling back to autodaily260108003006: - - # Find corrupted archive records - sqlite3 /opt/bambuddy/data/bambuddy.db \ - "SELECT id, filename, file_path FROM print_archives - WHERE file_path = '' OR file_path IS NULL - OR file_path NOT LIKE 'archive/%';" - - # Check all file_path values - sqlite3 /opt/bambuddy/data/bambuddy.db \ - "SELECT id, file_path FROM print_archives ORDER BY id;" - -CONTAINER 109 (this host): -- Were you also deleting archives around 11:22 and 12:22? -- Same bug could have been triggered here too - -PROXMOX COMMANDS FOR ROLLBACK: - # Container 107 (3dp) - pct rollback 107 autodaily260108003006 - pct start 107 - - # Container 109 (claude) - already done via UI - # Current snapshot: autodaily260108003004 - -WHAT TO DO NEXT: -1. Rollback container 107 to morning snapshot -2. Run the SQL query above to find corrupted archive -3. Apply the fix from container 109 to container 107 -4. Understand how the file_path got corrupted in the first place - -THE FIX (apply to both containers): -In backend/app/services/archive.py, the delete_archive function now has: -- Empty file_path check -- Path traversal protection (relative_to check) -- Minimum depth check (must be 2+ levels inside archive dir) -- Error logging for refused deletions - -NOT CLAUDE CODE'S FAULT: -This was a bug in BamBuddy's own code that was triggered by: -1. Corrupted database record (unknown how it got corrupted) -2. User action (deleting archives via web UI) diff --git a/deploy/bambuddy.service b/deploy/bambuddy.service index cf30af7aa..b096b3d29 100644 --- a/deploy/bambuddy.service +++ b/deploy/bambuddy.service @@ -6,8 +6,8 @@ After=network.target Type=simple User=claude Group=claude -WorkingDirectory=/opt/claude/projects/bambuddy -Environment="PATH=/opt/claude/projects/bambuddy/venv/bin" +WorkingDirectory=