# NCV DCIMS — NCV Dental Clinical Image Management System: cập nhật 0.2.29 → 0.2.30 bằng bộ r2

**Ngày 30/09/2026 — hướng dẫn chính của bộ r2.** r2 giữ phiên bản ứng dụng 0.2.30, sửa quyền đọc mã chương trình trong image API/migration và tổng hợp kinh nghiệm cập nhật NAS thực tế. Không đổi TypeScript/UI/SQL/Compose hoặc payload Volume 2 lịch sử so với r1.

**NAS hiện tại của chủ hệ thống đã xong:** migration, provision lại role, API/Web/Gateway healthy, DSM sync Volume 2, HTTPS smoke, Dockerfile source sửa quyền và đăng nhập/giao diện bình thường đều đã được xác nhận. **Không chạy các bước nâng cấp dưới đây lại trên NAS này.** Giữ bộ r2 cho lần build sau và bàn giao. Đây là hướng dẫn cập nhật site còn ở 0.2.29, không phải cài trắng hoặc chuyển volume.

## 1. Phạm vi và những thứ giữ nguyên

Theo task **Di chuyển share folder sang Volume 2** chốt 15/09 và hướng dẫn 0.2.25/0.2.27/0.2.28/0.2.29, dùng đúng topology sau. Nếu khác, dừng đối chiếu; không tự sửa hash/layout/UUID để vượt guard.

| Thành phần | Giữ nguyên |
| --- | --- |
| Project Container Manager | `ncv-dcims-production`, không tạo project mới |
| Source project | `/volume2/docker-v2/ncv-dcims-production` |
| Công cụ site/DSM sync | `/volume2/docker-v2/ncv-tools/dcims-v1` |
| PostgreSQL | 17; volume ngoài `ncv-dcims-postgres-data-v2`; device `/volume2/docker-v2/ncv-dcims-postgres-data-v2` |
| Media | `/volume2/ncv-dcims/media` |
| Ledger WORM | `/volume2/ncv-dcims-ledger-v2` |
| Backup ứng dụng | `/volume2/ncv-dcims/backup/repository`, receipts `/volume2/ncv-dcims/backup/receipts` |
| Sao lưu trước nâng cấp | Shared folder riêng tư `/volume2/migration-backup` |
| Docker Root / Container Manager | Volume 1; không move `/volume1/@docker` hoặc package |
| Domain/TLS/Cloudflare/IMS | Không đổi domain, TLS, trusted IP, tunnel hoặc IMS |
| Quyền/chính sách dữ liệu | Giữ UID/GID, secret chia sẻ/khóa backup, retention/purge/legal hold/scheduler |

Khác bản cũ: thêm migration và ba credential runtime/migration/backup; giữ file quản trị cũ. API không dùng credential quản trị. r2 sửa quyền **mã chương trình bên trong image**, không chmod/chown secrets/media/ledger/database trên host.

Trước cửa sổ bảo trì, owner vận hành xác nhận share mã hóa đã mở/mount, backup **database cùng media** và restore point dùng được theo chính sách đang duyệt. Dump của updater chỉ là database và kiểm parse, không thay backup media/offsite/restore drill. Không chạy đồng thời DSM Build/Update hoặc một phiên updater khác.

## 2. Upload và đăng nhập đúng nơi

| File / thư mục | Vị trí |
| --- | --- |
| ZIP | `ncv-dcims-production-0.2.30-complete-synology-r2.zip` |
| Checksum | `ncv-dcims-production-0.2.30-complete-synology-r2.zip.sha256.txt` |
| Upload trong File Station | Shared folder **docker-v2**, Location **Volume 2**, tạo thư mục **ncv-dcims-release-0.2.30-r2** |
| Đường dẫn SSH của thư mục upload | `/volume2/docker-v2/ncv-dcims-release-0.2.30-r2` |
| Source sau giải nén | `/volume2/docker-v2/ncv-dcims-release-0.2.30-r2/source` |

Không upload vào database/media/ledger/backup repository hoặc source project đang chạy. Không giải nén đè project. ZIP chứa source để build, không có image offline/credential/database; NAS cần registry/package registry.

Trong **Windows PowerShell**, mở thư mục chứa hai file, so sánh hash:

```powershell
Get-FileHash -Algorithm SHA256 .\ncv-dcims-production-0.2.30-complete-synology-r2.zip
Get-Content .\ncv-dcims-production-0.2.30-complete-synology-r2.zip.sha256.txt
```

Hai hash phải giống hệt nhau. Upload cả hai file vào đúng thư mục trong bảng.

Trong PowerShell, thay hai phần `<...>` bằng tài khoản quản trị và địa chỉ NAS thực tế:

```powershell
ssh '<tai-khoan-quan-tri-nas>@<dia-chi-nas>'
```

Sau đăng nhập, dấu nhắc phải là **`admin@NAS...:~$`**, không phải `PS C:\...>`. Tất cả khối `sh` sau đây chạy trên NAS. **Dán nguyên khối từ `sudo sh` đến dòng kết thúc `DCIMS_...`; không bỏ dòng cuối.** Dấu `>` trong lúc dán là shell đang chờ đủ khối. Sudo có thể hỏi password; nhập tại prompt, không viết password vào lệnh.

Mỗi khối tự khai báo đường dẫn và chạy trong shell sudo riêng, không phụ thuộc biến của khối trước. Không cần `sudo -i`. Khi khối lỗi, shell con dừng nhưng bạn vẫn ở NAS admin; **không chạy bước tiếp theo hoặc lặp bước vừa lỗi theo phỏng đoán**. Mất SSH: đăng nhập lại NAS, kiểm bước đã đạt trước khi tiếp tục.

## 3. Kiểm ZIP và hiện trạng trước giải nén

Khối này kiểm site còn ở 0.2.29; nếu NAS đang ở 0.2.30, không chạy quy trình nâng cấp.

```sh
sudo sh <<'DCIMS_PRECHECK_R2'
set -eu
export PATH=/var/packages/ContainerManager/target/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH
DCIMS_PROJECT=/volume2/docker-v2/ncv-dcims-production
DCIMS_STAGE=/volume2/docker-v2/ncv-dcims-release-0.2.30-r2
DCIMS_TOOLS=/volume2/docker-v2/ncv-tools/dcims-v1
cd "$DCIMS_PROJECT"
test -f .env
test ! -L .env
test "$(sed -n 's/^NCV_DCIMS_APP_VERSION=//p' .env | tr -d '\r')" = 0.2.29
test -d secrets
test -d /volume2/migration-backup
test "$(cat /volume2/docker-v2/ncv-dcims-postgres-data-v2/PG_VERSION)" = 17
sh "$DCIMS_TOOLS/dcims.sh" check </dev/null
docker ps -a --filter name=ncv-dcims --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
cd "$DCIMS_STAGE"
test ! -e source
test ! -e operator-state
tr -d '\r' < ncv-dcims-production-0.2.30-complete-synology-r2.zip.sha256.txt | sha256sum -c -
printf 'PRECHECK_R2_OK\n'
DCIMS_PRECHECK_R2
```

**Đạt:** checksum `OK`, `NCV_VOLUME2_READY version=0.2.29`, PostgreSQL/API/Web/Gateway healthy, `PRECHECK_R2_OK`. Tunnel có thể xuất hiện thêm. Adapter check có probe nhỏ ở media/backup và helper lưu cấu hình riêng tư; không gọi nó là hoàn toàn không ghi.

Trong **File Station**, chọn ZIP → **Extract/Giải nén** vào **source** bên trong thư mục upload. Nếu DSM tạo thư mục tên ZIP, đổi tên thành `source`. Phải có trực tiếp `source/package.json`, `source/apps/api/package.json`, `source/deploy`; không lồng thêm thư mục tên ZIP và không để source nằm ngay ở gốc staging. **Không dùng `unzip` trong SSH: NAS này không có lệnh đó.**

## 4. Sao lưu rồi chép source r2

App vẫn chạy. Khối tự lưu source/config/secrets/tools cũ, kiểm version/hash source và bảo đảm `.env`, `volume2.override.yml`, `docker-compose.yml` không bị thay. Đường dẫn backup được lưu trong `operator-state/source-backup.path`; bước sau đọc và kiểm lại, không phải gõ suffix từ ảnh.

```sh
sudo sh <<'DCIMS_SOURCE_R2'
set -eu
umask 077
export PATH=/var/packages/ContainerManager/target/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH
DCIMS_PROJECT=/volume2/docker-v2/ncv-dcims-production
DCIMS_STAGE=/volume2/docker-v2/ncv-dcims-release-0.2.30-r2
DCIMS_TOOLS=/volume2/docker-v2/ncv-tools/dcims-v1
cd "$DCIMS_PROJECT"
test -f .env
test ! -L .env
test "$(sed -n 's/^NCV_DCIMS_APP_VERSION=//p' .env | tr -d '\r')" = 0.2.29
test ! -e "$DCIMS_STAGE/operator-state"
for dcims_manifest in package.json apps/api/package.json apps/web/package.json; do
  test -s "$DCIMS_STAGE/source/$dcims_manifest"
  grep -Eq '"version"[[:space:]]*:[[:space:]]*"0[.]2[.]30"' "$DCIMS_STAGE/source/$dcims_manifest"
done
test -s "$DCIMS_STAGE/source/deploy/site/nas-volume2/install-dcims-security-update.sh"
for dcims_private in .env secrets volume2.override.yml docker-compose.yml; do
  test ! -e "$DCIMS_STAGE/source/$dcims_private"
  test ! -L "$DCIMS_STAGE/source/$dcims_private"
done
test "$(tr -d '\r' < "$DCIMS_STAGE/source/deploy/docker/api.Dockerfile" | sha256sum | cut -d ' ' -f 1)" = 43f6cc05d3a78f6da7bd726a01eeb5a6e5328a99973e6ec4cafa6886b90d98a1
mkdir "$DCIMS_STAGE/operator-state"
DCIMS_SAVE=$(mktemp -d /volume2/migration-backup/dcims-source-before-0230-XXXXXX)
printf 'SOURCE_BACKUP=%s\n' "$DCIMS_SAVE"
mkdir "$DCIMS_SAVE/config"
cp -p .env compose.production.certified.yml volume2.override.yml docker-compose.yml "$DCIMS_SAVE/config/"
cp -a secrets "$DCIMS_SAVE/secrets"
cp -a "$DCIMS_TOOLS" "$DCIMS_SAVE/site-tools"
tar -cf "$DCIMS_SAVE/source-before.tar" \
  --exclude=node_modules --exclude=dist --exclude=generated \
  --exclude=.next --exclude=.wrangler --exclude=coverage \
  apps packages patches deploy scripts \
  package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json README.md .dockerignore
for dcims_file in compose.*.yml .env.*.example .gitignore AGENTS.md; do
  if test -f "$dcims_file"; then cp -p "$dcims_file" "$DCIMS_SAVE/config/"; fi
done
test -s "$DCIMS_SAVE/source-before.tar"
tar -tf "$DCIMS_SAVE/source-before.tar" >/dev/null
cp -a "$DCIMS_STAGE/source/." "$DCIMS_PROJECT/"
for dcims_config in .env volume2.override.yml docker-compose.yml; do
  cmp "$DCIMS_PROJECT/$dcims_config" "$DCIMS_SAVE/config/$dcims_config"
done
printf '%s\n' "$DCIMS_SAVE" > "$DCIMS_STAGE/operator-state/source-backup.path"
printf 'SOURCE_R2_READY_PRIVATE_CONFIG_UNCHANGED\n'
DCIMS_SOURCE_R2
```

**Đạt:** `SOURCE_R2_READY_PRIVATE_CONFIG_UNCHANGED`. Lưu `SOURCE_BACKUP`; thư mục có secrets thật, không gửi cả thư mục. `operator-state` đã có mà khối lỗi: giữ trạng thái, kiểm backup/copy; không xóa marker rồi lặp bước. Không `rsync --delete`, không quét Docker Root, không chép payload site đè `ncv-tools/dcims-v1`. Mẫu `.env.production.example` không thay `.env` thật. Source mới chưa làm container đang chạy restart.

## 5. Tạo credential và tự cập nhật năm khóa `.env`

Không dùng `vi`, không in URL/password, không gõ đường dẫn file quản trị theo phỏng đoán. Khối dò file cũ từ `.env`, yêu cầu chưa có ba file mới, build migration image rồi kiểm pnpm/Prisma bằng UID thật **trước downtime**. Nó tạo ba credential khác nhau, giữ file quản trị cũ và thay đúng năm khóa `.env` bằng file tạm sau khi đã sao lưu.

```sh
sudo sh <<'DCIMS_ENV_R2'
set -eu
umask 077
export PATH=/var/packages/ContainerManager/target/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH
DCIMS_PROJECT=/volume2/docker-v2/ncv-dcims-production
DCIMS_STAGE=/volume2/docker-v2/ncv-dcims-release-0.2.30-r2
cd "$DCIMS_PROJECT"
test -f .env
test ! -L .env
DCIMS_SAVE=$(cat "$DCIMS_STAGE/operator-state/source-backup.path")
case "$DCIMS_SAVE" in /volume2/migration-backup/dcims-source-before-0230-*) ;; *) exit 1;; esac
test -s "$DCIMS_SAVE/source-before.tar"
test -d "$DCIMS_SAVE/secrets"
for dcims_config in .env volume2.override.yml docker-compose.yml; do
  cmp "$dcims_config" "$DCIMS_SAVE/config/$dcims_config"
done
read_setting() {
  test "$(awk -F= -v key="$1" '$1==key {n++} END {print n+0}' .env)" = 1 || { printf "STOP: MISSING_OR_DUPLICATE_KEY %s\n" "$1" >&2; return 1; }
  sed -n "s/^$1=//p" .env | tr -d '\r'
}
test "$(read_setting NCV_DCIMS_APP_VERSION)" = 0.2.29
DCIMS_OLD_DB_FILE=$(read_setting NCV_DCIMS_DATABASE_URL_FILE)
test -n "$DCIMS_OLD_DB_FILE"
case "$DCIMS_OLD_DB_FILE" in
  /*) DCIMS_ADMIN_FILE="$DCIMS_OLD_DB_FILE";;
  ./*) DCIMS_ADMIN_FILE="$DCIMS_PROJECT/${DCIMS_OLD_DB_FILE#./}";;
  *) DCIMS_ADMIN_FILE="$DCIMS_PROJECT/$DCIMS_OLD_DB_FILE";;
esac
test -s "$DCIMS_ADMIN_FILE"
DCIMS_RUNTIME_USER="$(read_setting NCV_DCIMS_RUNTIME_UID):$(read_setting NCV_DCIMS_RUNTIME_GID)"
test "$DCIMS_RUNTIME_USER" = 1044:65549
for dcims_role in runtime migration backup; do
  test ! -e "secrets/$dcims_role-database-url.txt"
  test ! -L "secrets/$dcims_role-database-url.txt"
done
docker build -f deploy/docker/api.Dockerfile --target migration -t ncv-dcims-api-migration:0.2.30 . </dev/null
docker run --rm --network none --read-only --user "$DCIMS_RUNTIME_USER" \
  --cap-drop ALL --security-opt no-new-privileges:true \
  --tmpfs /tmp:rw,noexec,nosuid,size=64m \
  -e COREPACK_ENABLE_NETWORK=0 -e CHECKPOINT_DISABLE=1 \
  -e DATABASE_URL=postgresql://127.0.0.1:5432/ncv_dcims_permission_check \
  --entrypoint /bin/sh ncv-dcims-api-migration:0.2.30 \
  -ec 'test -r package.json; pnpm --version; pnpm exec prisma --version' </dev/null
printf 'MIGRATION_TOOLCHAIN_READ_OK_BEFORE_DOWNTIME\n'
docker run --rm --network none --user 0:0 --entrypoint node \
  --mount "type=bind,src=$DCIMS_ADMIN_FILE,dst=/old-admin-url,readonly" \
  --mount "type=bind,src=$DCIMS_PROJECT/secrets,dst=/role-secrets" \
  ncv-dcims-api-migration:0.2.30 dist/scripts/generate-database-role-secrets.js \
  --admin-url-file /old-admin-url --output-directory /role-secrets </dev/null
for dcims_role in runtime migration backup; do test -s "secrets/$dcims_role-database-url.txt"; done
for dcims_key in NCV_DCIMS_DB_ADMIN_URL_FILE NCV_DCIMS_MIGRATION_DATABASE_URL_FILE NCV_DCIMS_BACKUP_DATABASE_URL_FILE; do
  test "$(grep -c "^$dcims_key=" .env || true)" -le 1
done
DCIMS_ENV_TMP=$(mktemp "$DCIMS_PROJECT/.env.update-r2.XXXXXX")
trap 'rm -f "$DCIMS_ENV_TMP"' 0
trap 'exit 130' INT
trap 'exit 143' HUP TERM
cp -p .env "$DCIMS_ENV_TMP"
sed \
  -e '/^NCV_DCIMS_APP_VERSION=/d' \
  -e '/^NCV_DCIMS_DB_ADMIN_URL_FILE=/d' \
  -e '/^NCV_DCIMS_DATABASE_URL_FILE=/d' \
  -e '/^NCV_DCIMS_MIGRATION_DATABASE_URL_FILE=/d' \
  -e '/^NCV_DCIMS_BACKUP_DATABASE_URL_FILE=/d' \
  .env > "$DCIMS_ENV_TMP"
printf '\n%s\n' 'NCV_DCIMS_APP_VERSION=0.2.30' >> "$DCIMS_ENV_TMP"
printf 'NCV_DCIMS_DB_ADMIN_URL_FILE=%s\n' "$DCIMS_OLD_DB_FILE" >> "$DCIMS_ENV_TMP"
printf '%s\n' \
  'NCV_DCIMS_DATABASE_URL_FILE=./secrets/runtime-database-url.txt' \
  'NCV_DCIMS_MIGRATION_DATABASE_URL_FILE=./secrets/migration-database-url.txt' \
  'NCV_DCIMS_BACKUP_DATABASE_URL_FILE=./secrets/backup-database-url.txt' >> "$DCIMS_ENV_TMP"
for dcims_key in NCV_DCIMS_APP_VERSION NCV_DCIMS_DB_ADMIN_URL_FILE NCV_DCIMS_DATABASE_URL_FILE NCV_DCIMS_MIGRATION_DATABASE_URL_FILE NCV_DCIMS_BACKUP_DATABASE_URL_FILE; do
  test "$(grep -c "^$dcims_key=" "$DCIMS_ENV_TMP")" = 1
done
mv -f "$DCIMS_ENV_TMP" .env
printf 'ENV_R2_READY\n'
DCIMS_ENV_R2
```

**Đạt:** `MIGRATION_TOOLCHAIN_READ_OK_BEFORE_DOWNTIME`, `DB_ROLE_SECRET_FILES_CREATED`, `ENV_R2_READY`. Toolchain probe không kết nối DB. Tạo file không đổi password administrator hay provision role. File credential mới có quyền ban đầu 0600; updater chuẩn hóa quyền các file được chỉ định.

Nếu ba file mới đã có hoặc khối lỗi giữa chừng, không xóa/sinh lại và không sửa `.env` bằng mẫu; dừng đối chiếu bước đã đạt. Mọi khóa khác được giữ, bao gồm domain/TLS, storage, UID/GID, khóa chia sẻ/backup và chính sách dữ liệu.

## 6. Cài revision adapter rồi update một lần

Chỉ chạy khi bước 3–5 đạt, có cửa sổ bảo trì và backup theo mục 1. Installer có guard riêng cho topology/layout/helper/UID/DSM; nó chỉ cài revision cần cho credential mới, không phải installer Volume 2 gốc. Không stop toàn project trước khối này.

```sh
sudo sh <<'DCIMS_UPDATE_R2'
set -eu
umask 077
export PATH=/var/packages/ContainerManager/target/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH
cd /volume2/docker-v2/ncv-dcims-production
sh deploy/site/nas-volume2/install-dcims-security-update.sh </dev/null
sh /volume2/docker-v2/ncv-tools/dcims-v1/dcims.sh check </dev/null
sh /volume2/docker-v2/ncv-tools/dcims-v1/dcims.sh update 0.2.30 </dev/null
DCIMS_UPDATE_R2
```

**Đạt:** adapter ready và **`NCV_VOLUME2_UPDATE_OK version=0.2.30 backup=...`**. Lưu backup path. `NCV_VOLUME2_READY` riêng lẻ chỉ là kiểm sẵn sàng, không phải đã cập nhật.

Updater build khi app cũ vẫn chạy → kiểm secret/quyền/storage/role input → dừng đúng Gateway/API/Web, giữ PostgreSQL → dump/parse → provision role → migration → provision lại để chặn runtime đọc metadata migration/sửa xóa audit → recreate ba app, kiểm healthy/image → đồng bộ DSM. PostgreSQL và tunnel không được nâng/recreate trong bước này. Build có thể lâu ở download/export layers; không mở phiên build thứ hai.

## 7. Kiểm sau cập nhật và UI

```sh
sudo sh <<'DCIMS_VERIFY_R2'
set -eu
export PATH=/var/packages/ContainerManager/target/usr/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH
cd /volume2/docker-v2/ncv-dcims-production
sh /volume2/docker-v2/ncv-tools/dcims-v1/dcims.sh check </dev/null
sh /volume2/docker-v2/ncv-tools/dcims-v1/sync-dsm-compose.sh check </dev/null
docker ps -a --filter name=ncv-dcims --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
test "$(tr -d '\r' < deploy/docker/api.Dockerfile | sha256sum | cut -d ' ' -f 1)" = 43f6cc05d3a78f6da7bd726a01eeb5a6e5328a99973e6ec4cafa6886b90d98a1
DCIMS_HOST=$(sed -n 's/^NCV_DCIMS_PUBLIC_HOST=//p' .env | tr -d '\r')
test -n "$DCIMS_HOST"
sh scripts/synology/ncv-dcims-post-update-smoke-test.sh "https://$DCIMS_HOST" 0.2.30 </dev/null
DCIMS_VERIFY_R2
```

**Đạt:** `NCV_VOLUME2_READY version=0.2.30`, `DSM_COMPOSE_SYNC_CHECK_OK`, bốn service healthy và **`NCV_DCIMS_POST_UPDATE_SMOKE_OK`**. Adapter kiểm image pins/volume/UID/layout; HTTPS smoke kiểm health/auth boundary/header, không đọc phiên bản public. Không gửi `.env`, credential hoặc log có dữ liệu thật.

Mở web, **Ctrl+F5**, đăng nhập; kiểm Cài đặt không icon trước tiêu đề, menu/nút vẫn dùng được, paging/tìm/lọc bệnh nhân và Back/Forward. Nghiệm thu role/scope/share/upload/confirm/reopen chỉ dùng tài khoản và dữ liệu thử được phép; không xóa ca thật để thử thu hồi link. Kiểm responsive 390/768/1440 và thiết bị Back/Forward thực khi nghiệm thu UI đầy đủ. Đăng nhập/giao diện bình thường không thay DR/offsite/reboot/restore UAT.

## 8. Các lỗi đã gặp và cách xử lý đúng

| Hiện tượng | Nguyên nhân / xử lý |
| --- | --- |
| Sudo disabled trong Windows | SSH đã đóng và đang ở `PS>`. SSH lại NAS; không bật sudo Windows để sửa |
| Biến rỗng, lệnh tìm `/source/...` | Biến mất khi đổi root/admin/SSH. Các khối r2 tự khai báo đầy đủ; không tiếp tục dán đoạn cũ |
| Không tìm thấy ZIP hoặc sidecar | Upload sai folder/tên hoặc đang đứng sai thư mục. Kiểm hai file đúng bảng mục 2, checksum OK trước giải nén |
| `UNZIP_NOT_AVAILABLE` | Dùng File Station, không cài thêm unzip hoặc lặp lệnh lỗi |
| Có package.json ngay staging root | Giải nén sai đích. Cần `.../source/package.json`; chưa copy vào project |
| SOURCE_READY được in dù test trước lỗi | In một marker riêng không chứng minh đạt. Chỉ dùng marker cuối của nguyên khối đã dừng khi lỗi |
| Backup suffix từ ảnh bị đọc sai | r2 lưu đường dẫn từ mktemp vào operator-state và so config, không gõ tên ngẫu nhiên từ ảnh |
| Pnpm báo bản mới hoặc request chậm | Không tự nâng package manager; giữ pnpm 11.9.0/lockfile. Chờ và kiểm tiến trình; không build song song |
| Corepack EACCES đọc package.json | Source giải nén giữ 0600/0700, UID site khác node. r2 sửa quyền program workspace; toolchain probe trước downtime. Không chmod toàn shared folder, không chạy API root |
| Update lỗi stage=migration/activation | Role/schema có thể đã đổi. Không bật container/image cũ, restore hoặc chạy full updater lại theo phỏng đoán |
| App healthy nhưng DSM sync lỗi | Kiểm riêng helper/DSM/default Compose; không rebuild/migrate lại chỉ để sửa sync |
| `update.lock` đã tồn tại | Kiểm tiến trình/trạng thái; không xóa lock để ép chạy |

Nếu lỗi: giữ nguyên output và backup path, ghi bước/marker cuối; không thực hiện bước sau. Trước migration, kiểm trạng thái thực rồi mới quyết định tiếp tục. Sau role/migration, phương án rollback/restore theo schema phải được chủ hệ thống duyệt; **Không tự động trả image cũ hoặc restore database** vì có thể không tương thích/mất dữ liệu. Không purge, `down -v`, Clean/Delete project, prune, migrate reset/resolve thủ công hoặc seed DB vận hành. Không chạy Docker system df/quét Docker Root nặng từng gây treo NAS.

Nhật ký và khối xử lý sự cố ZIP gốc/r1 nằm ở [hướng xử lý lỗi quyền image](synology-recover-permissions-0.2.30.md), chỉ dùng khi trạng thái khớp và được đối chiếu; không phải phần bắt buộc của upgrade r2. [Bằng chứng kiểm](../evidence/release-0.2.30-verification-2026-09-30.md) phân biệt chuẩn bị local, NAS thực và phần chưa kiểm. Fresh full Docker build từ source sửa chưa được chạy lại local vì engine unavailable/WSL bị từ chối; NAS đã kiểm candidate images với cùng logic chmod, migration/runtime/smoke và source checksum.
