Перейти до змісту

Оновлення та міграція

Цей посібник -- оператор-протокол безпечного оновлення. Схема БД рухається тільки вперед -- автоматичної down-міграції немає. Якщо треба повернутись назад, відновлюйтесь із резервної копії.

Завжди робіть backup data/ (або тома bamdude_data у Docker) перед будь-яким оновленням. А саме: bamdude.db (або dump PostgreSQL, якщо у вас PG), директорія archive/ (3MF + мініатюри) та директорія library/.


1. Чек-ліст перед оновленням

Перед тим, як щось чіпати:

  1. Зупиніть сервіс BamDude (sudo systemctl stop bamdude або docker compose down).
  2. Зробіть backup директорії з даними -- див. команди backup нижче.
  3. Запишіть свою поточну версію -- відкрийте /system/health у браузері або виконайте cat pyproject.toml | grep version для нативних інсталяцій. Корисно, якщо доведеться відкочуватись.
  4. Якщо ви за reverse proxy (nginx / Caddy / Traefik), скопіюйте конфіг убік, щоб перевірити його після оновлення.
  5. Перевірте розмір логів -- якщо data/logs/ величезна, це гарний момент її зротейтити.

Команди backup

# Том даних -- sqlite БД, архіви, мініатюри, аплоади
docker run --rm \
  -v bamdude_data:/from \
  -v "$(pwd)/backup":/to \
  alpine tar czf /to/bamdude-data-$(date +%Y%m%d).tar.gz -C /from .

# Логи (опційно)
docker run --rm \
  -v bamdude_logs:/from \
  -v "$(pwd)/backup":/to \
  alpine tar czf /to/bamdude-logs-$(date +%Y%m%d).tar.gz -C /from .

Відкрийте Налаштування → Backup → Локальна резервна копія → Створити резервну копію, потім Завантажити резервну копію, щоб зберегти zip собі на комп. Zip пакує SQLite БД, директорію архіву, мініатюри, аплоади та конфіг у тому самому layout-і, який install.sh розкладає на диску -- відновлення -- це просто "розпакувати в шлях інсталяції та перезапустити". Він також захоплює метадані ключа шифрування та стан запланованих бекапів, які tar сирого data/ лишає позаду.

cd /opt/bamdude
tar czf ~/bamdude-data-$(date +%Y%m%d).tar.gz data/
pg_dump -Fc -f ~/bamdude-$(date +%Y%m%d).dump "$DATABASE_URL"
# Плюс затарте директорії archive/ + library/ з тома даних.

2. Процедура оновлення -- Docker

cd bamdude
docker compose pull          # якщо запіноване на :latest
docker compose up -d
docker compose logs -f       # дивитись, як застосовуються міграції

Пінити конкретний тег у compose.yaml -- це ок і навіть рекомендовано для стабільних інсталяцій -- :0.4.1 ніколи не зрушить; :latest йде за main.

# Запіноване, рекомендовано
image: ghcr.io/kainpl/bamdude:0.4.1

# Rolling, йде за main
image: ghcr.io/kainpl/bamdude:latest

Слідкуйте за стартовим логом для прогресу міграцій. Довгі міграції логують пакетний прогрес (напр. m020 library_files: progress на m020/m022). Чекайте на "Migrations complete", перш ніж тестувати.

INFO  [backend.app.migrations] Applying m019_archive_queue_batch_error
INFO  [backend.app.migrations] Applied m019 (version 19)
INFO  [backend.app.migrations] Applying m022_label_object_metadata_backfill
INFO  [backend.app.migrations] m022 library_files: progress 100/847
INFO  [backend.app.migrations] m022 library_files: progress 200/847
...
INFO  [backend.app.migrations] Applied m022 (version 22)
INFO  [backend.app.main] Startup complete

Sanity-check /system/health повертає 200.


3. Процедура оновлення -- Manual (Python venv)

cd /opt/bamdude
sudo systemctl stop bamdude

# Підтягнути сорси
sudo -u bamdude git fetch
sudo -u bamdude git checkout v0.4.1     # або який там тег

# Python-залежності
sudo -u bamdude ./venv/bin/pip install -r requirements.txt --upgrade

# Frontend-бандл (регенерує треканий каталог static/).
# Пропустіть цей крок, якщо завжди тягнете pre-built теги -- бандл їде
# in-tree на release-коміті. Потрібно лише якщо збираєте з кастомної гілки.
sudo -u bamdude bash -c 'cd frontend && npm ci && npm run build'

# Перезапустити та хвостити логи
sudo systemctl start bamdude
sudo journalctl -u bamdude -f

Поставлений install/update.sh автоматизує всю послідовність (stop → backup → git pull → pip install → npm build → start) і підтримує env-перевизначення:

sudo /opt/bamdude/install/update.sh
Змінна Дефолт Призначення
INSTALL_DIR /opt/bamdude Де живе BamDude
SERVICE_NAME bamdude systemd-юніт для рестарту
BRANCH поточна вичекаутена гілка Перемкнутись на іншу гілку під час оновлення
BACKUP_MODE auto auto пропускає, коли нема чого бекапити, require обриває, якщо backup впав, skip вимикає
FORCE 0 Виставити 1, щоб обійти перевірки dirty-worktree / backup

4. Процедура оновлення -- In-app апдейтер (сторінка Інформація)

Для native-інсталів найпростіший шлях -- in-app апдейтер. У боковому меню пункт Інформація (route /system) → Check for updates показує latest реліз; кнопка Install Update під ним виконує повну послідовність (git fetch --tags, git reset --hard refs/tags/<release>, pip install, npm run build) не виходячи з UI.

Toggles на тій самій панелі:

  • Check for updates automatically -- вимкни щоб перестати polling-увати GitHub.
  • Include beta updates -- показувати vX.Y.ZbN релізи теж. За замовчуванням off. Апдейтер поважає або version-name convention, або GitHub-prerelease-флаг -- реліз явно marked-prerelease у GitHub UI теж сховається за цей toggle.

Docker-інсталам in-app Install Update відмовлено -- запуск git fetch / pip install / npm build у запущеному контейнері зіпсує image. Замість того панель surface-ить дві бокові секції з resolved-тегом pre-filled: image-based (pull && up -d з правильним рядком image:, з beta-специфічним hint що :latest бети не трекає), і source-build (git fetch --tags && git checkout v<tag> && compose build --pull && up -d). Скопіюй один із блоків залежно від install-shape -- повний reference у System Info → Update checker.

In-app апдейтер захищений від pre-release tag mismatch, у який 0.4.3-та-раніше тихо потрапляли: клік Install Update на бета-релізі тихо no-op'ив, бо apply path робив hard-reset до origin/main, що не несе бета-тег. Виправлено у 0.4.4.


5. Огляд міграцій -- що міняє кожна версія

BamDude трекає застосовані міграції в таблиці _migrations. Кожен реліз запускає всі очікуючі версії по порядку при першому завантаженні. Нові інсталяції спочатку запускають create_all() (створюючи таблиці з поточних означень моделей), потім m000 + m001 пре-проштамповуються як застосовані через bootstrap-крок, і фактично виконуються тільки пізніші міграції.

Міграції, помічені seed, містять DML-крок (бекфіл / нормалізація даних) і можуть займати помітний час на великих інсталяціях; чисто-DDL міграції (додавання колонок, swap FK) завершуються за мілісекунди.

Версія Назва Що змінює Seed Вперше потрібна в
m000 bambuddy_to_bamdude_301 Інертна з 0.5.6 -- не робить нічого. Раніше імпортувала спадковий bambuddy.db / bambutrack.db. Запис версії 0 збережений, бо bootstrap-крок штампує його на кожній наявній інсталяції. Повідомлення про файл Bambuddy -- це стартова перевірка, а не ця міграція: міграція виконується один раз, а повідомлення має повторюватись, поки файл лежить на місці. no --
m001 bamdude_baseline Створює FTS-індекс для пошуку по архівах (FTS5 на SQLite, tsvector + GIN на PostgreSQL) та засіює початкові довідкові дані (каталог моделей принтерів, дефолтні групи тощо). yes Свіжі інсталяції BamDude
m002 bamdude_311 Bump схеми BamDude 3.0.1 → 3.1.1. Додає printer_queues, macros, колонки swap mode, конфіг stagger, таблиці історії обслуговування, переробку черги (queue_id), printer_models на типах обслуговування. Дропає мертву таблицю filaments. yes Оновлення з BamDude 3.0.x
m003 enforce_admin_user Кодифікує always-on модель авторизації: штампує auth_enabled=true + setup_completed=true, якщо існує хоча б один admin; інакше чистить обидва прапорці, щоб наступне завантаження направило користувача через /setup. Схема не змінюється. yes Усі інсталяції
m004 m002_reconcile Перезапускає m002.upgrade() дослівно. Ловить інсталяції, що застрягли на ранній версії m002 (до правила frozen-migrations), де версія була позначена як застосована, але пізніші правки m002 ніколи не виконались. yes Застряглі post-3.1.1 інсталяції
m005 swap_profiles Друга вимірність на swap mode: printers.swap_profile + macros.swap_profile. Перепривʼязує наявні вбудовані макроси A1 Mini до swap_profile='a1mini_kit'; засіває порожні вбудовані для a1mini_stl + jobox-a1. yes Усі інсталяції
m006 mesh_mode_fast_check Додає print_queue.mesh_mode_fast_check BOOLEAN DEFAULT 1, щоб оператор міг відмовитись від bed-mesh fast-check probe для кожного елемента черги. no Усі інсталяції
m007 drop_vibration_cali Дропає print_queue.vibration_cali (Bambu Studio тепер хардкодить це у false для кожної моделі; живе тільки в калібрувальному візарді). MQTT-payload усе ще емітить ключ для сумісності з прошивкою. no Усі інсталяції
m008 swap_macro_queue_fields Додає print_queue.execute_swap_macros BOOLEAN DEFAULT 1 + swap_macro_events TEXT (JSON array), щоб кожен елемент черги міг перевизначити, які swap-події для нього стріляють. no Усі інсталяції
m009 archive_source_hash Додає print_archives.source_content_hash (SHA256 непатченого джерела) + applied_patches (JSON). Запити дедупу перемикаються на COALESCE(source_content_hash, content_hash), щоб BamDude-патчені архіви дедупились проти оригіналів з бібліотеки. no Усі інсталяції
m010 queue_reliability Додає print_archives.subtask_id VARCHAR(64) (advisory archive matching через перезапуски) + printers.awaiting_plate_clear BOOLEAN DEFAULT 0 (персистоване plate-clear ворота, переживає Auto Off power-cycle). no Усі інсталяції
m011 cloud_region Додає users.cloud_region VARCHAR(10), щоб per-user облікові дані Bambu Cloud несли свій регіон. Закриває cross-tenant витік регіону, який мав singleton-сервіс. no Усі інсталяції
m012 mfa Кластер MFA / 2FA / OIDC -- шість нових таблиць: user_totp, user_otp_codes, auth_ephemeral_tokens, auth_rate_limit_events, oidc_providers, user_oidc_links, плюс users.password_changed_at. Підтримує always-on модель авторизації з 0.4.0. no 0.4.0
m013 library_file_print_count Додає library_files.print_count INTEGER DEFAULT 0. Per-file лічильник завершених друків, інкрементується в on_print_complete. no 0.4.0
m014 archive_library_link Додає print_archives.library_file_id FK (ON DELETE SET NULL) + бекфілить його на кожному наявному архіві шляхом hash-метчингу проти library_files.file_hash. Перераховує library_files.print_count та last_printed_at з історії завершених архівів (перезаписує попередні значення -- історія архівів є авторитативною). yes 0.4.0
m015 refresh_token_support Додає auth_ephemeral_tokens.used_at + family_id для підтримки sliding-session refresh-флоу (§18.14). Reuse-detection відкликає всю фемілі, якщо refresh-токен реплейнуть. no 0.4.0
m016 project_print_plan Створює project_print_plan_items (per-project упорядкований список .3mf-файлів зі степером копій). Бекфілить один рядок на кожен наявний linked library_files.project_id з copies=1. yes 0.4.0
m017 macro_action_type Додає macros.action_type + mqtt_action + delay_seconds. Дозволяє макросу інвокувати MQTT-команду (chamber_light_off, chamber_light_on) замість gcode на події print_started / print_finished з опційною затримкою. no 0.4.0
m018 queue_library_fk_set_null Змінює FK print_queue.library_file_id з ON DELETE CASCADE на ON DELETE SET NULL. У комбінації з in-app каскадом у delete_file це дає SQLite ту саму поведінку, що PostgreSQL отримує нативно. no 0.4.0
m019 archive_queue_batch_error Рефакторинг queue↔archive. Додає print_archives.queue_id (FK, indexed) + batch_id (VARCHAR(36), indexed) + error_message (TEXT). Дропає чотири кешовані лічильники з printer_queues (completed_count / failed_count / cancelled_count / total_count). Бекфілить queue_id/batch_id/error_message з наявних посилань print_queue.archive_id. Видаляє завершені елементи черги, що мають архівне посилання -- бекфіл-еквівалент нового авточищення в on_print_complete. yes 0.4.0
m020 spool_purchase_date Додає три колонки в spool: purchase_date DATETIME, filament_diameter VARCHAR(8) NOT NULL DEFAULT '1.75', lot INTEGER. Бекфілить filament_diameter у '1.75' (дефолт Bambu). yes 0.4.0 (post-b2)
m021 drop_auto_light_off Дропає спадкову колонку printers.auto_light_off. Замінено фреймворком макросів (сконфігуруйте mqtt-action макрос chamber_light_off на події print_started для того самого ефекту, плюс опційний симетричний chamber_light_on на print_finished). no 0.4.0
m022 label_object_metadata_backfill Відкриває кожен наявний на диску 3MF, витягає gcode_label_objects + exclude_object з Metadata/project_settings.config, вмерджує їх у library_files.file_metadata та print_archives.extra_data. Довгий старт на першому завантаженні, якщо у вас багато архівів -- див. §5 Помітні шляхи оновлення. yes 0.4.1
m023 per_plate_metadata_backfill Відкриває кожен 3MF на диску ще раз і серіалізує повний per-plate breakdown (plates[] payload + is_multi_plate flag) у ті ж самі JSON-колонки library_files.file_metadata та print_archives.extra_data. Це робить можливою per-plate galleryв File Manager + multi-plate UI у PrintModal без перевідкривання 3MF на кожен запит списку. Той самий профіль довгого старту, що й m022 — запускається один раз. yes 0.4.1

5. Помітні шляхи оновлення

З Bambuddy HE 3.0.x → BamDude 0.4.x

Ваш bambuddy.db -- це власний файл BamDude: стартове перейменування робить із нього bamdude.db, m002 адаптує схему, m005+ -- BamDude-нативні. (Видалення імпорту Bambuddy 2.2.2 у 0.5.6 цього не стосується -- воно завжди було лише про файли апстріму.)

Завжди оновлюйтесь до 0.4.0.1 або пізніше

Перехід зі спадкової інсталяції 3.0.1 одразу на 0.4.0 падав на m005_swap_profiles.seed() з no such column: printers.awaiting_plate_clear -- seed використовував ORM-овий select(Printer), який підвантажував кожну колонку з поточної моделі, включно з колонками, яких на момент m005 у ланцюжку ще не існує. Виправлено в 0.4.0.1 переписуванням seed на raw SQL з явними списками колонок.


Перехід з Bambuddy

Не підтримується з 0.5.6. BamDude відгалузився від Bambuddy на 2.2.2, і схеми розійшлись надто далеко, щоб одноразовому імпорту можна було довіряти, тож імпортер прибрано.

Стартуйте BamDude з порожньою текою даних і додайте принтери та котушки заново.

bambuddy.db, залишений у теці даних, не читається і не чіпається. Стартап BamDude називає його в лозі на кожному старті, тож ви бачите, що файл знайдено й проігноровано; видаліть його самі, коли він більше не потрібен.

Це не те саме, що оновлення з Bambuddy HE / BamDude 3.0.x

Це власна лінія BamDude, і вона й далі підтримується -- див. Помітні шляхи оновлення вище. bambuddy.db, записаний BamDude 3.0.1, розпізнається як власний файл BamDude і перейменовується на bamdude.db, точно як раніше.


Зміна способу інсталяції

Можна змінити спосіб інсталяції в будь-який момент без зачіпання даних -- просто наведіть нову інстанцію на існуючу директорію data/ або скопіюйте вміст тому.

Native → Docker

sudo systemctl stop bamdude

# Скопіюйте native-дані в Docker-том
docker volume create bamdude_data
docker run --rm \
  -v /opt/bamdude/data:/from \
  -v bamdude_data:/to \
  alpine cp -a /from/. /to/

# Стартуйте GHCR-образ проти нового тому
docker run -d --name bamdude --network host \
  -v bamdude_data:/app/data -v bamdude_logs:/app/logs \
  --restart unless-stopped ghcr.io/kainpl/bamdude:latest

# Лише після того, як ви переконались, що Docker-інстанція працює, вимкніть/видаліть native-сервіс:
sudo systemctl disable bamdude

Docker → Native

docker compose down

# Скопіюйте том на диск
docker run --rm \
  -v bamdude_data:/from \
  -v "$(pwd)/extracted":/to \
  alpine cp -a /from/. /to/

# Встановіть native, націлений на витягнуті дані
sudo ./install/install.sh --data-dir "$(pwd)/extracted" --yes

Docker Hub → GHCR (або навпаки)

Лише обмін реєстром, дані не торкаємо:

# docker-compose.yml
# image: kainpl/bamdude:latest      ← Docker Hub
# image: ghcr.io/kainpl/bamdude:latest  ← GitHub Container Registry
docker compose pull
docker compose up -d

Обидва реєстри публікують ті самі теги. GHCR -- основне джерело (білдиться в CI на кожному релізі); Docker Hub -- дзеркало.


6. Перевірка після оновлення

Коли сервіс знову на ходу:

  1. /system/health повертає 200.
  2. Параметри → Система → версія відображає новий реліз.
  3. Підключіться до принтера, який працював до оновлення -- має реконектнутись за 30 секунд; перевірте картку принтера на сторінці Принтери.
  4. Відкрийте кілька найновіших архівів -- мініатюри мають усе ще рендеритись, 3D-перегляд -- працювати, клік на іконку принтера -- стрибати на принтер-власник.
  5. Тригерніть диспатч на двох принтерах одночасно -- тост у нижньому правому куті має показати, як обидва завдання прогресять паралельно. Фаза DB-insert ненадовго серіалізована (startup-lock), але FTP-завантаження + старт відбуваються одночасно. Див. Черги для кожного принтера → Поведінка диспатчу.
  6. Перелогіньтесь (якщо оновлюєтесь з 0.3.x → 0.4.x), щоб видали refresh-token cookie і перебрав на себе sliding-session флоу.

Фрагменти лог міграції, які корисно грепнути:

INFO  [backend.app.migrations] Applied m019 (version 19)
INFO  [backend.app.migrations] Applied m022 (version 22)
INFO  [backend.app.main] Startup complete

Індикатори падіння:

ERROR  [backend.app.migrations] Migration mXXX failed: ...
sqlite3.OperationalError: no such column: ...

no such column / no such table на старті майже завжди означає, що міграція не виконалась -- зазвичай це проблема дозволів файлової системи на data/. Полагодьте через sudo chown -R bamdude:bamdude /opt/bamdude/data і перезапустіть.


7. Відкат (якщо щось зламалось)

Оскільки схема рухається тільки вперед, план відкату завжди -- відновіть до-оновлювальний backup. Автоматичної down-міграції немає -- ви не можете, наприклад, "відмінити" m019 archive↔queue рефакторинг на місці. Відновлення з backup -- єдиний шлях.

docker compose down

# Витерти вміст нового тома
docker volume rm bamdude_data
docker volume create bamdude_data

# Відновити з backup-тарбола
docker run --rm \
  -v "$(pwd)/backup":/from \
  -v bamdude_data:/to \
  alpine sh -c 'cd /to && tar xzf /from/bamdude-data-YYYYMMDD.tar.gz'

# Запіньте compose на попередній Docker-тег перед стартом:
# image: ghcr.io/kainpl/bamdude:0.4.0
docker compose up -d

На свіжій інсталяції старого тегу, після first-run setup, відкрийте Налаштування → Backup → Локальна резервна копія, Завантажте скачаний zip, потім перезапустіть сервіс. Zip відновлює БД + архіви + аплоади + конфіг одним заходом.

sudo systemctl stop bamdude
cd /opt/bamdude
sudo rm -rf data
sudo tar xzf ~/bamdude-data-YYYYMMDD.tar.gz
sudo -u bamdude git checkout v0.4.0       # або ваш попередній тег
sudo -u bamdude ./venv/bin/pip install -r requirements.txt
sudo systemctl start bamdude
docker compose down       # або зупиніть нативний сервіс
pg_restore -c -d "$DATABASE_URL" ~/bamdude-YYYYMMDD.dump
# Потім відновіть archive/ + library/ з тарбола даних.
docker compose up -d      # з image, запіненим на попередній тег

Версія, на яку ви відкочуєтесь, має бути тією, що створила backup -- інакше схема в БД буде новіша за те, що очікує цей код, і старт зафейлиться помилкою column-not-found на першому ж читанні.

Forward-only -- це навмисно

Down-міграції потребували б шляхів коду, яких BamDude не несе, -- відновлення з backup структурно простіше і завжди коректне. Docker-тег :0.4.0 лишається запіненим безстроково, тож ви завжди можете відкотитись на нього.


8. Нотатки про backend БД

SQLite (дефолт)

Файл БД живе за data/bamdude.db. Pragma SQLite: WAL journal, 15 с busy timeout, NORMAL synchronous. WAL означає, що поруч із головним файлом є ще bamdude.db-wal та bamdude.db-shm -- бекапте всі три разом (або зупиніть сервіс перед цим, щоб WAL було чекпойнтнуто в головний файл).

Якщо bambuddy.db (або bambutrack.db), записаний BamDude 3.0.1, існує в директорії даних, а bamdude.db -- ні, BamDude розпізнає його як власний і перейменовує на bamdude.db при першому завантаженні, перед тим як запуститься хоч одна міграція -- саме так нативні інсталяції, що міняють бінарник на місці, переносять свої дані далі. Файл апстрім-Bambuddy не перейменовується і не читається -- див. Перехід з Bambuddy.

PostgreSQL — вбудований або власний

DATABASE_URL має три стани, і перемикання між ними — це теж міграція:

DATABASE_URL Бекенд
порожньо / не задано SQLite у data/bamdude.db
embedded PostgreSQL 18, який іде разом із BamDude, у DATA_DIR/postgres/18
postgresql+asyncpg://… ваш власний сервер

Наявні PG-інсталяції запускають той самий ланцюжок міграцій на кожному старті — та сама таблиця _migrations, ті самі версії, та сама послідовність. Хелпери діалекту маршрутизують DDL через PG-нативні шляхи там, де SQLite потребує recreate_table (зміни FK, дроп колонок), а PG ензорсить FK-обмеження, які SQLite пропускає мовчки (m018 — хороший приклад, де SET NULL реально спрацьовує лише на PG).


Перехід зі SQLite на PostgreSQL

Це одноразове автоматичне копіювання, однакове для вбудованого сервера і для власного.

  1. Спершу зробіть резервну копію (див. Команди backup). Це єдиний крок, який ніхто за вас не зробить.
  2. Задайте DATABASE_URL — embedded або URL вашого сервера. Для зовнішнього сервера база вже має існувати; BamDude створює таблиці, а не бази.
  3. Перезапустіть BamDude.

На цьому старті BamDude бачить PostgreSQL без даних (свіжу базу) поруч із наявним bamdude.db і переносить усе. У журналі:

Found local SQLite database at .../bamdude.db, migrating to PostgreSQL
...
SQLite -> PostgreSQL migration complete (78 tables). Original renamed to bamdude.db.migrated

Далі проти PostgreSQL відпрацьовує звичайний ланцюжок міграцій, і застосунок піднімається. На даних реальної ферми саме копіювання займає близько хвилини-двох; довше зазвичай іде ланцюжок схеми після нього.

Що саме переноситься

Усі таблиці й рядки, з конвертацією типів (SQLite 0/1 → boolean, рядки дат → timestamps). Послідовності автоінкременту скидаються у правильні значення, повнотекстовий індекс перебудовується як tsvector + GIN у PostgreSQL. Віртуальні таблиці FTS5, файли WAL/SHM і облік міграцій не копіюються — вони або специфічні для SQLite, або створюються наново.

Рядки-сироти видаляються, і це пишеться в журнал

PostgreSQL ензорсить зовнішні ключі, яких SQLite ніколи не перевіряв, тож рядки, що вказують на вже неіснуючі записи, перенести неможливо. Імпортер їх чистить і прямо каже, що саме видалив, наприклад:

Purging 3 orphan label_jobs rows before import
Purging 5 orphan spool_usage_history rows
Purging 51 orphan smart_plug_energy_snapshots rows

Так задумано — ці рядки й так були недосяжні. Але прочитайте ці рядки: якщо кількість виглядає дивно для вашої інсталяції, зупиніться й відновіться з резервної копії, а не йдіть далі.

Невдалий імпорт зупиняє старт, а не продовжує його

Якщо копіювання зламалося на півдорозі, BamDude зупиняється з помилкою, а не продовжує як свіжа інсталяція. PostgreSQL лишається як є, а bamdude.db не перейменовується, тож ваші дані у SQLite цілі: достатньо прибрати DATABASE_URL і перезапуститись, щоб опинитись рівно там, де були. (Старіші збірки могли мовчки продовжити з порожньою базою — саме тому перейменування відбувається лише після повністю успішного копіювання.)

Повернення на SQLite

Приберіть DATABASE_URL (або лишіть порожнім) і перейменуйте файл назад:

mv data/bamdude.db.migrated data/bamdude.db

Усе, що з'явилося після переходу, живе лише в PostgreSQL, тож якщо це треба зберегти — спершу зробіть резервну копію з працюючого PostgreSQL. Формат копії з інтерфейсу переносний і відновлюється на будь-який бекенд.

Вбудований сервер (DATABASE_URL=embedded) під час оновлень

Шлях Що містить
DATA_DIR/postgres/18/ кластер, у каталозі з назвою за мажором PostgreSQL
DATA_DIR/postgres/password згенерований пароль (права 0600)
DATA_DIR/postgres/port порт, на якому він піднявся, якщо ви не запінили EMBEDDED_PG_PORT

Копіюйте це разом із рештою data/ — кластер це просто файли у вашому каталозі даних, тож tar каталогу data/ при зупиненому сервісі є валідною копією. Для переносної копії, яка відновиться на будь-який бекенд, користуйтесь Налаштування → Резервні копії.

Мажор PostgreSQL зафіксований

BamDude відмовиться відкривати кластер, створений іншим мажором PostgreSQL, і скаже про це прямо, не чіпаючи дані:

the data directory ... was created by PostgreSQL 17, but the bundled server is
PostgreSQL 18. Refusing to start: a major upgrade needs a migration step, not a
silent open.

Новий мажор вийде окремим релізом із явним кроком міграції. Звичайні оновлення BamDude у межах того самого мажора не потребують від вас нічого.

Оновлюйтесь при зупиненому сервері

На нативній інсталяції pip install -r requirements.txt не зможе замінити пакет вбудованого PostgreSQL, поки з нього працює сервер — зупиніть BamDude (він зупинить і сервер) перед оновленням залежностей. install/update.sh уже зупиняє службу першим кроком.

Windows: дві схеми служб

Windows-інсталятор пропонує вбудований сервер або окремою службою BamDudePostgres (менеджер служб піднімає її перед BamDude), або дочірнім процесом BamDude. Повторний запуск інсталятора лишає ту схему, якою ви вже користуєтесь — він читає поточний бекенд із середовища встановленої служби й передвибирає його, тож оновлення ніколи не поверне вас мовчки на SQLite. Деінсталяція зупиняє й видаляє обидві служби і окремо питає (за замовчуванням Ні), чи видаляти ваші дані.

Docker

  • DATABASE_URL=embedded піднімає вбудований сервер усередині контейнера BamDude, а його кластер лежить у наявному томі bamdude_data в підкаталозі postgres/. У штатному compose-файлі на docker compose down дається 60 с, щоб він устиг зробити контрольну точку — не скорочуйте це значення.
  • Для PostgreSQL в окремому контейнері є штатний override docker-compose.postgres.yml. Зверніть увагу на застереження про host-мережу в Підтримці PostgreSQL: зі штатним network_mode: host BamDude не дістане інший контейнер за іменем сервісу Compose.

9. Персистентність даних -- "новий контейнер запустився порожнім"

Найпоширеніша катастрофа під час оновлення -- запустити свіжий BamDude-контейнер і виявити, що немає принтерів, немає архіву, немає налаштувань -- наче чиста інсталяція. Дані майже ніколи не пропадають насправді; вони все ще в Docker volume або container layer'і, з якого новий екземпляр просто не читає. Цей розділ покриває кожну причину, яку ми бачили, і фікс для кожної.

Швидка діагностика

Запустіть це на хості і прочитайте, що виведе. Скрипт перебирає BamDude-пов'язані контейнери, кожен volume, який потенційно тримає дані, розмір кожного volume і mount-розкладку будь-якого "старого" контейнера, який ви залишили як бекап.

echo "=== Контейнери ==="
docker ps -a --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.CreatedAt}}" \
  | grep -iE "bamdude|bambuddy"
echo

echo "=== Volume'и ==="
for v in $(docker volume ls -q | grep -iE "bamdude|bambuddy"); do
  size=$(docker run --rm -v "$v":/d alpine du -sh /d 2>/dev/null | awk '{print $1}')
  printf "%-50s  %s\n" "$v" "$size"
done
echo

echo "=== Mount'и старого контейнера (заміни 'bamdude-old' на реальне ім'я) ==="
docker inspect bamdude-old --format '{{range .Mounts}}{{.Type}}: {{.Source}} → {{.Destination}}{{"\n"}}{{end}}'
echo

echo "=== Вміст DATA_DIR старого контейнера ==="
docker exec bamdude-old sh -c 'echo "DATA_DIR=$DATA_DIR"; ls -la "$DATA_DIR" 2>/dev/null | head'

Здоровий data-volume має щонайменше десятки MB (SQLite + thumbnails) і зазвичай GB з історією архівів. Свіжий / порожній volume -- максимум кілька сотень KB. Цей контраст зазвичай за дві секунди показує, де реально лежать дані.


Сценарій A -- змінилася назва Compose-проєкту (найпоширеніший)

Docker Compose v2 неймспейсує кожен named-volume за назвою проєкту, а та за замовчуванням -- basename директорії, де лежить compose-файл. Volume bamdude_data у вашому docker-compose.yml стає <project>_bamdude_data на диску:

Розкладка Реальна назва volume
~/bambuddy/docker-compose.yml (upstream Bambuddy) bambuddy_bambuddy_data
~/bamdude/docker-compose.yml bamdude_bamdude_data
~/bamdude-new/docker-compose.yml bamdude-new_bamdude_data
~/3d/bamdude/docker-compose.yml (з COMPOSE_PROJECT_NAME=3d) 3d_bamdude_data

Перейменування compose-папки (mv ~/bamdude ~/bamdude-old) і розпакування свіжого checkout'у на оригінальному шляху створює зовсім новий неймспейс, і docker compose up -d створює порожній bamdude_bamdude_data, поки ваші реальні дані сидять у bamdude-old_bamdude_data. Обидва volume'и видно через docker volume ls; дані тільки в одному.

Фікс -- спрямувати новий проєкт на існуючий volume:

Найчистіший шлях -- оголосити існуючий volume як external у новому compose-файлі, щоб Docker Compose не намагався ним керувати:

services:
  bamdude:
    # ... без змін ...
    volumes:
      - bamdude_data:/app/data
      - bamdude_logs:/app/logs

volumes:
  bamdude_data:
    external: true
    name: bamdude-old_bamdude_data    # volume з вашими даними
  bamdude_logs:
    external: true
    name: bamdude-old_bamdude_logs    # аналогічно для логів

Після docker compose up -d новий контейнер читає/пише той самий фізичний volume, що й старий. Коли переконаєтесь, що все працює, можна зупинити старий контейнер і звільнити його ім'я.

Або -- скопіювати дані в новий volume:

Якщо хочете тримати неймспейсинг нового проєкту чистим і завершити з єдиним <new>_bamdude_data volume:

# Зупинити новий контейнер, щоб не гонявся з копіюванням.
docker compose down

# Перестворити (порожній) цільовий volume для безпеки.
docker volume rm <new>_bamdude_data
docker volume create <new>_bamdude_data

# Одноразова копія через одноразовий alpine-контейнер, який монтує обидва volume'и.
docker run --rm \
  -v bamdude-old_bamdude_data:/from:ro \
  -v <new>_bamdude_data:/to \
  alpine sh -c 'cp -a /from/. /to/'

# Підняти новий compose-проєкт.
docker compose up -d

Сценарій B -- container layer (volume взагалі немає)

Якщо оригінальна інсталяція була через голий docker run ghcr.io/kainpl/bamdude:latest без -v bamdude_data:/app/data, всі записи приземлились у writable layer контейнера. Перейменування цього контейнера через docker rename зберігає layer (і відповідно дані), але запуск свіжого контейнера з того самого образу створює новий layer. Новий layer = немає bamdude.db, немає архіву.

Виявити це можна через docker inspect <old_container> і перевірити .Mounts. Якщо немає запису з mount'ом /app/data, дані в layer.

Фікс -- витягти дані, потім переїхати на нормальну volume-конфігурацію:

# Скопіювати дані з layer старого контейнера на хост.
docker cp bamdude-old:/app/data ./bamdude-data-recovered

# Створити нормальний named-volume і засіяти його.
docker volume create bamdude_bamdude_data    # відповідно до неймспейсу нового проєкту
docker run --rm \
  -v "$(pwd)/bamdude-data-recovered":/from:ro \
  -v bamdude_bamdude_data:/to \
  alpine sh -c 'cp -a /from/. /to/'

# Використати офіційний compose-файл -- він завжди декларує volume.
cd ~/bamdude
docker compose up -d

Надалі завжди декларуйте volume (named або bind-mount) для /app/data і /app/logs. Виданий docker-compose.yml це робить; голі docker run команди потребують явного -v.


Сценарій C -- змінився шлях bind-mount

Якщо ваш compose використовував bind-mount (./data:/app/data або /srv/bamdude:/app/data) замість named-volume, переміщення compose-папки також переміщує таргет bind-mount'а. Нова інсталяція приземлюється на свіжому порожньому шляху.

volumes:
  - ./data:/app/data    # шлях ВІДНОСНИЙ ДО COMPOSE-ФАЙЛА

Виявити через docker inspect <container> --format '{{range .Mounts}}{{.Source}}{{"\n"}}{{end}}'. Якщо з'являється шлях /srv/... чи /home/..., це там реально лежать дані.

Фікс -- скопіюйте host-директорію в нове місце або наведіть новий compose на старий шлях:

# Варіант 1: перемістити bind-директорію.
mv ~/bamdude-old/data ~/bamdude/data
docker compose up -d

# Варіант 2: залишити дані де є, навести новий compose на той шлях.
# Відредагувати volumes: у docker-compose.yml на абсолютний старий шлях:
#   - /home/user/bamdude-old/data:/app/data
docker compose up -d

Сценарій D -- розбіжність PUID / PGID (дані є, але app не може читати)

Виданий compose запускає контейнер як ${PUID:-1000}:${PGID:-1000}. Якщо ваш старий контейнер працював як 1000:1000, а новий запуск використовує інші ID (наприклад, ви виставили PUID=$(id -u) у shell, де id -u != 1000), файли volume належать UID, який новий процес не може читати чи писати. Симптоми різноманітні:

  • Стартові логи показують PermissionError: [Errno 13] Permission denied: '/app/data/bamdude.db'
  • Або app тихо відкочується на свіжу БД поряд із недоступною старою
  • Або init_db падає на першому записі міграції

Перевірте власника:

docker run --rm -v bamdude_bamdude_data:/d alpine ls -ln /d

Числові UID / GID у третій і четвертій колонках мають збігатись із тим, що повертають id -u / id -g для юзера, як яким працює новий контейнер.

Фікс -- chown вмісту volume на нові ID:

docker compose down
docker run --rm -v bamdude_bamdude_data:/d alpine chown -R 1000:1000 /d
# Або який PUID:PGID у вашому compose зараз.
docker compose up -d

Dockerfile вже робить chmod 777 на /app/data під час білда, тож Docker bind-mounted директорії наслідують той ліберальний режим. Named-volume забирає ownership першого writer'а, тому одноразового chown достатньо.


Сценарій E -- docker compose down -v (volume'и видалено)

Прапорець -v на docker compose down назавжди видаляє named-volume'и проєкту. Якщо ви запустили це перед оновленням ("про всяк випадок"), дані зникли з точки зору Docker -- немає Recycle Bin. Перейменований старий контейнер допомагає тільки якщо він був запущений з bind-mount (host-директорія вижила) або тримав дані в container layer (Сценарій B).

Sanity-check: docker volume ls | grep bamdude має показати і старі volume'и, якщо вони існують. Якщо з'являються тільки щойно створені і жоден не має GB-розміру, дані видалено.

Відновлення -- з application-level бекапа:

Єдине надійне відновлення в цьому випадку -- BamDude UI backup zip, який ви мали зробити перед оновленням за §1. Флоу:

  1. Підняти новий BamDude (він приземлиться у setup-required стан).
  2. Пройти first-run setup з тимчасовими credentials -- наступний крок все одно замінить БД.
  3. Відкрити Settings → Backup → Local Backup → Upload Backup і вибрати pre-upgrade zip.
  4. Перезапустити контейнер, щоб система міграцій підлаштувалася під відновлену БД.

Це відновлює bamdude.db, архіви, library-файли, thumbnails, uploads і кожен Settings-рядок. Зашифровані секрети (TOTP, OIDC client_secret) відновлюються коректно, бо backup несе метадані того MFA_ENCRYPTION_KEY, яким вони були зашифровані -- той самий ключ має бути у environment нового контейнера, інакше ці рядки не розшифруються.

Якщо у вас немає бекапа і жоден з попередніх сценаріїв не підходить, дані не можна відновити.


Сценарій F -- GUI Docker-менеджер (Portainer / Dockge / Komodo) створює власний неймспейс

GUI Docker-менеджери часто створюють власний Compose-проєкт при імпорті -- "stacks" Portainer'а стають проєктами з іменем стека, Dockge монтує кожен compose під /opt/stacks/<name> і використовує те ім'я як назву проєкту. Імпорт того ж compose-файла під іншою назвою стека дає свіжий volume-неймспейс точно як у Сценарії A.

Фікс ідентичний Сценарію A: оголосити існуючий volume як external і навести на нього за реальним іменем. Запустіть docker volume ls, щоб побачити, яку назву створив GUI і яку використовувала ваша стара інсталяція.


Сценарій G -- Образ змінив DATA_DIR між версіями (рідко є причиною)

BamDude поставляється з ENV DATA_DIR=/app/data з першого Docker-релізу і ми ніколи його не міняли -- цей сценарій задокументований для повноти на випадок, якщо ви оновлюєтесь з приватного форка чи кастомного образу. Якщо ви колись перенесли дані в /data замість /app/data у власному образі, volume, змонтований на старому шляху, не підхопиться новим образом. Перенести дані:

docker run --rm \
  -v <volume>:/v \
  alpine sh -c 'mkdir -p /v/app/data && mv /v/data/* /v/app/data/ 2>/dev/null'

Або просто перемонтувати volume на новий шлях, який очікує образ:

volumes:
  - bamdude_data:/app/data    # не /data

Чому наш legacy-DB rename не завжди спрацьовує

Стартап BamDude (migrations/__init__.py) перейменовує bambuddy.db / bambutrack.db на bamdude.db -- але тільки тоді, коли файл є власною базою BamDude епохи 3.0.1, яку він розпізнає за таблицею telegram_chats усередині. Справжній файл апстрім-Bambuddy лишається недоторканим і лише називається в лозі; див. Перехід з Bambuddy. Для файла 3.0.1 перейменування далі спрацьовує тільки тоді, коли legacy-файл всередині /app/data нового контейнера -- тобто коли volume mount правильний. Якщо новий контейнер читає зі свіжого порожнього volume (Сценарії A, C, D, E), немає legacy-файла, який можна перейменувати в принципі; логіка перейменування взагалі не релевантна.

Фікс завжди має одну форму: змусити новий контейнер читати з volume, який тримає ваші дані -- або вказавши на існуючий volume (external: true), або скопіювавши дані в новий.


Траблшутінг

Стартовий лог показує setup_required 503 на кожному ендпоінті
Перше завантаження не створює admin. Відкрийте / у браузері, щоб пройти setup-флоу. Це нормально для свіжих інсталяцій і після кожного cli reset_admin.
no such column / no such table на старті
Міграція не виконалась. Перевірте лог на стек-трейс; зазвичай це означає, що файлові дозволи на data/ не дають сервіс-юзеру писати. Полагодьте через sudo chown -R bamdude:bamdude /opt/bamdude/data.
База Bambuddy лежить у data/, і з нею нічого не сталось
Тепер це очікувана поведінка. Імпорт прибрано в 0.5.6 -- стартап BamDude називає файл у лозі на кожному старті й лишає його в спокої. Ніщо його не імпортує; видаліть, коли більше не потрібен.
Копіювання Docker volume падає з device or resource busy
Спочатку зупиніть і source, і destination контейнер. Контейнер alpine з --rm, що монтує обидва тома, не може ділити файлову систему з працюючим сервісом, що тримає відкриті файли.
Native update лишає сервіс зламаним
update.sh пише backup до того, як щось чіпає (/opt/bamdude/backups/pre-update-YYYYMMDD-HHMMSS/). Зупиніть сервіс, відновіть директорію backup поверх data/ та вичекаутіть попередній git-тег.
Довга пауза на першому завантаженні 0.4.1
Це m022 обходить кожен 3MF на диску. Хвостіть лог -- ви маєте бачити рядки m022 library_files: progress N/M кожним пакетом по 100. Не вбивайте процес; перезапуск просто продовжить з того місця, де відкомітився останній пакет.
database is locked посеред міграції
Ви запустили сервіс до того, як попередня інстанція повністю зупинилась. Зупиніть, дочекайтесь виходу старого процесу (перевірте через pgrep -f bamdude / docker compose ps), потім стартуйте знову. Система міграцій ідемпотентна -- міграції, що впали посеред, чисто перезапускаються при наступному завантаженні.

Що нового в 0.4.x

Можливість Опис
Per-Printer Queues Незалежна черга для кожного принтера з картковим UI; quantity > 1 пропускає кожну копію через чергу (без особливої "primary").
Рефакторинг queue↔archive Жива черга авточиститься; історія черги живе на print_archives (m019).
Паралельний диспатч Кілька принтерів отримують завдання одночасно. Коротка фаза DB-write обгорнута в startup-lock (там залишається серіалізація), щоб SQLite не гонився на INSERT INTO print_archives; усе інше — FTP-завантаження, MQTT-команда старту — паралельно. Тимчасовий "один за раз через усю ферму" gate, що приземлився в середині 0.4.1, прибрали, як тільки startup-lock у дисптачер заїхав.
Sliding-session auth TTL access JWT -- 1 г; ротуючий refresh-cookie тримає юзерів залогіненими прозоро. Remember-me опт-іниться на 30-денну персистентність.
MFA + OIDC TOTP, email OTP, 10 backup-кодів, OIDC SSO з PKCE + JWKS + SSRF-захистом. Шифрується at rest з MFA_ENCRYPTION_KEY.
MQTT-action макроси Макроси можуть інвокувати MQTT-команду (chamber_light_off / chamber_light_on) на print_started / print_finished з опційною затримкою. Заміняє спадковий прапорець auto_light_off.
Per-project print plan Кожен проєкт несе впорядкований список своїх .3mf-файлів бібліотеки зі степером копій, per-row totals та смугою grand-totals.
Відновлення завантаження 3MF Fallback-архіви авто-заповнюються через FTP, коли принтер був недосяжний на старті друку.
Метадані label-object Прапорці підтримки skip-objects, витягнуті з Metadata/project_settings.config і збережені на кожному файлі бібліотеки + архіві (m022).
Server-side нарізання (0.4.2b3) OrcaSlicer + BambuStudio sidecar-контейнери в одному Compose-проєкті (--profile orca / --profile bambu / --profile all), вибір слайсера на кожен запит у Slice-діалозі з live-індикаторами доступності, override типу столу (Cool / Engineering / High-Temp / Textured PEI / SuperTack), inline-вибір плити для мульти-плейт-файлів, owner-фільтр на пресетах.
Композитні file_tags (0.4.2b3) JSON-колонка library_files.file_tags керує баджами + чіп-фільтром у File Manager: format (gcode / 3mf / stl / obj / step), readiness (sliced / project / geometry), modifiers (swap / multiplate), provenance (makerworld). m036 + m037 заповнюють історичні рядки.
Per-plate awareness в архівах (0.4.2b3) Мульти-плейт-архіви тепер запам'ятовують, яка саме плита 3MF друкувалась; thumbnail, інфо про друк, G-code preview і 3D-модель — усе про цю плиту. m038 бекфілить plate_index на існуючих рядках і перепарсить 3MF, де plate_index > 1, щоб оновити slicer-derived колонки + thumbnail.
Library viewer capabilities + правильний стіл (0.4.2b3) Новий ендпоінт /library/files/{id}/capabilities (дзеркало архівного) керує видимістю 3D / G-code вкладок через file_tags замість сканування суфіксів файлу; 3D-в'ювер тепер малює напівпрозорий вайрфрейм друкарського об'єму, що відповідає реальному столу принтера (раніше було захардкожено 256³).

Див. CHANGELOG.md для детальностей по версіях.