---
title: "Changelog"
url: https://phatdev.net/docs/sl-migrations/changelog
lang: vi
updated: 2026-10-20
alternate:
  en: https://phatdev.net/en/docs/sl-migrations/changelog
markdown: https://phatdev.net/docs/sl-migrations/changelog.md
---

# Changelog

Thuộc bộ tài liệu: [SL Migrations](https://phatdev.net/docs/sl-migrations.md)

> Tất cả các thay đổi đáng chú ý của plugin SL Migrations được ghi lại tại đây.

## CHANGELOG - SL Migrations

Tất cả các thay đổi đáng chú ý của plugin **SL Migrations** được ghi lại tại đây.
Theo định dạng [Keep a Changelog](https://keepachangelog.com/vi/1.1.0/).

---

## [1.8.10] - 2026-10-18 (Sửa lỗi: nâng cấp làm mất cấu hình đích lưu SFTP/FTP)

### Sửa lỗi
- **Sau khi nâng lên 1.8.9, một site không đẩy được backup lên đích SFTP** (log ghi cấu hình SFTP bị trống, thất bại nhiều lần). Nguyên nhân: site chưa có hằng số `SL_MIGRATIONS_AUTH_KEY` trong wp-config nên cấu hình đích lưu được mã hoá bằng legacy key (md5 của `AUTH_KEY` + `DB_HOST` + `DB_NAME` + `DB_USER`). Bước nâng cấp (`UpgradeFunctions::storeDupSecureKey()`) thấy thiếu hằng số liền tạo **khoá ngẫu nhiên mới** mà không mã hoá lại dữ liệu cũ, nên mọi đích lưu không giải mã được và rơi về giá trị mặc định.
  - `src/Core/Upgrade/UpgradeFunctions.php`: khi **nâng cấp** mà wp-config chưa có hằng số thì ghi chính legacy key (`createWpConfigSecureKey(false, true)`); chỉ khi **cài mới** mới tạo khoá ngẫu nhiên. Site đã có hằng số thì không đổi gì (không ghi đè). Đánh dấu `2026-10-18 (TD, 1.8.10)`.
  - Site bị ảnh hưởng: chỉ site chưa có hằng số này (các site khác đã có từ trước). Đã khôi phục bằng cách ghi lại legacy key vào `SL_MIGRATIONS_AUTH_KEY`; cấu hình SFTP giải mã lại được, không mất dữ liệu.

---

## [1.8.9] - 2026-10-18 (Bảo mật - không cho tải bản backup qua đường dẫn trực tiếp)

### Bảo mật
- **Bản backup (`*_archive.daf`, kèm toàn bộ database) tải được trực tiếp qua HTTP** nếu biết tên file - `wp-content/backups-slm/` nằm trong webroot và nginx không chặn; tên file log `*_log.txt` (tải được) trùng tên gốc với archive nên suy ra được tên archive. Mỗi bản backup nằm local vài giờ trước khi xác minh bản trên đích từ xa rồi xoá.
  - **Máy chủ:** thêm luật nginx (trong cấu hình chung hoặc cấu hình riêng của site) chặn mọi file trong `backups-slm/`, `backups-dup-pro/`, `backups-dup-lite/` (trả 404), chỉ chừa `*_log.txt` để nút **Xem log** / trang **Diagnostics → Log** trong admin vẫn chạy (log không chứa mật khẩu).
  - **Plugin:** `classes/package/class.pack.php` (`getLocalPackageFileURL()`, nhánh Archive) - nút **Tải backup** trong admin luôn dùng `getLocalPackageAjaxDownloadURL()` (admin-ajax, token HMAC-SHA256, hết hạn sau 1 giờ) thay vì URL trực tiếp `backups-slm/<archive>`; không phụ thuộc nginx có chặn hay không. Đánh dấu `// 2026-10-18 (TD)`.
- Không đổi luồng build/upload: SLM không tự gọi HTTP vào `backups-slm/` khi tạo gói; upload lên đích từ xa đi qua SFTP.

---

## [1.8.8] - 2026-08-15 (Installer - sửa lỗi CREATE TABLE khi collation mặc định rỗng, MariaDB 11.8)

### Sửa lỗi (installer)
- **CREATE TABLE báo "syntax error near '' at line N" với bảng dùng collation mới `utf8mb4_uca1400_ai_ci` của MariaDB 11.8** - `installer/slm-installer/classes/database/class.db.functions.php` (`getDefaultCollateOfCharset()`): khi collation nguồn bị coi là không hỗ trợ và collation mặc định của charset (`charsetData[$charset]['defCollation']`) trả về RỖNG, `QueryFixes::legacyCharsetAndCollation()` thay collation nguồn bằng chuỗi rỗng → câu lệnh thành `COLLATE=` (không giá trị) → MariaDB báo syntax error, bảng không được tạo (vd `wp_po_plugins`). Thêm fallback: khi `defCollation` rỗng, trả `{charset}_general_ci` (collation hợp lệ phổ quát) để không bao giờ sinh `COLLATE=` rỗng.

---

## [1.8.7] - 2026-08-15 (Installer - sửa lỗi kẹt vòng lặp trang nhập mật khẩu)

### Sửa lỗi (installer)
- **Nhập đúng mật khẩu vẫn không qua bước - cứ nằm ở khung mật khẩu (loop)** - `installer/slm-installer/src/Core/Params/Descriptors/ParamDescSecurity.php`: param `PARAM_SECURE_OK` (cờ "đã xác thực mật khẩu") khai `'persistence' => false`. Khi mật khẩu đúng, `Security::securityCheck()` set `PARAM_SECURE_OK = true` rồi `save()`, nhưng vì `persistence=false` nên giá trị **không được ghi ra file params** → request kế tải lại về mặc định `false` → `getSecurityType()` trả `SECURITY_PASSWORD` lần nữa → quay lại trang nhập mật khẩu vô hạn. Mật khẩu SAI vẫn báo sai đúng như cũ (không phải lỗi so khớp). Đổi thành `'persistence' => true` để cờ xác thực được giữ qua các request → nhập đúng mật khẩu là qua bước ngay. Bug nằm trong installer nhúng vào gói → cần **tạo lại gói backup** để gói mới dùng installer đã sửa.

---

## [1.8.6] - 2026-06-30 (Installer UX - sửa lỗi 404 cuối restore + cải tiến trang Info)

### Admin - trang Info (tab Thông tin)
- **Changelog phân trang** - `views/info/main.php` + `assets/css/info-page.css`: danh sách changelog dài giờ chia **5 phiên bản / trang** kèm thanh điều hướng (prev/next + số trang), JS thuần không phụ thuộc thư viện.
- **Server Timezone hiển thị đúng** - `views/info/main.php`: trước hiện `UTC` (timezone PHP của server). Nay lấy theo cấu hình WordPress qua `wp_timezone()` → hiển thị `UTC+7` (kèm tên vùng nếu site đặt `timezone_string`).

### Sửa lỗi (installer)
- **Installer báo lỗi 404 / "ajax error" dù restore đã thành công** - `installer/slm-installer/ctrls/classes/class.ctrl.s3.funcs.php` (`cleanupFiles()`): bản trước hard-delete artifact + `rrmdir(DUPX_INIT)` xóa nguyên folder `slm-installer/` (gồm `main.installer.php` + CSRF token) ngay tại chunk `STEP_CLEANUP_TMP_FILES` của step 3. Nhưng sau đó vẫn còn `STEP_SET_FILE_PERMS`, `STEP_FINAL_REPORT_NOTICES` và **step 4 final report** - mỗi cái là một POST riêng tới `main.installer.php` → server trả *"Primary script unknown" (404)* → UI hiện lỗi đỏ dù data đã restore đủ.
  - **Vá:** bỏ block tự hủy sớm. Việc xóa file installer được dời sang giai đoạn **không bị race**: `MigrationMng::autoCleanFileAfterInstall()` chạy ở lần đăng nhập admin đầu tiên (`PARAM_AUTO_CLEAN_INSTALLER_FILES` / `cleanInstallerFiles`, **mặc định BẬT**), xóa `installer.php` + `slm-installer/` + các artifact an toàn khi không còn bước nào cần. Đánh dấu `// NOTE (SLM 1.8.6)`.
- **Tên hiển thị installer sai** - `classes/package/class.pack.installer.php`, `installer/slm-installer/src/Bootstrap/BootstrapUtils.php`, `.../templates/default/pages-parts/boot-error/header.php`: brand name render ra `SL Migrationsfessional`. Sửa lại đúng `SL Migrations` (tên này nhúng vào archive lúc build gói nên phải build lại package mới có hiệu lực).

## [1.8.5] - 2026-06-25 (Restore Fixes)

### Sửa lỗi (restore)
- **Version check luôn fail** - `installer/slm-installer/src/Core/Security.php` (~241): `version_dup (1.8.x) !== DUPX_VERSION (placeholder 1.0.0)` luôn đúng → báo *"version of archive different from PHP scripts"*. Vá: thêm `&& false` bỏ check (fork tự đóng gói). Đánh dấu `// SLM-FIX`.
- **DB password không persist khi validate** - `.../Core/Params/Descriptors/ParamDescDatabase.php` (~181): `PARAM_DB_PASS` `'persistence' => false` → request validate DB đọc pass rỗng → *"Access denied … using password: NO"*. Vá: `'persistence' => true`. Đánh dấu `// SLM-FIX`.

> 2 patch đã có sẵn trong code (ghi ở tệp ghi chú patch) nhưng trước đây chưa bump version/CHANGELOG - entry này đồng bộ lại. Password gate (`getSecurityType()`) giữ nguyên.

## [1.8.4] - 2026-06-01 (Branding · Asset Rebrand)

### Branding - thay thế toàn bộ asset visual còn sót từ upstream

- **Email logo** (`assets/img/email-logo.png`) - thay bằng logo SL Migrations: icon hai ô vuông xanh + mũi tên + chữ "SL Migrations / BACKUP · RESTORE". Logo mới render bằng PHP GD + DejaVu Bold TTF, kích thước 280×64 px.
- **Brand preview** (`assets/img/dpro-brand.png`) - thay bằng ảnh demo installer SL Migrations 860×229 px (dark header, step wizard, form fields đúng style plugin).

Sau bản này không còn asset nào mang visual identity của upstream trong plugin.

---

## [1.8.3] - 2026-05-25 (WP 7.0 Compat · Merge Security+Performance Audit)

### Fix - Download buttons broken trên WordPress 7.0

- **HTML attribute escaping** - `wp_json_encode()` bên trong `onclick="..."` sinh ra dấu `"` không escape → SyntaxError JS. Wrap `esc_attr(wp_json_encode(...))` tại:
  - `slm-modern/views/_row.php` - nút Installer, Archive, Both
  - `slm-modern/views/detail.php` - rewrite nút download dùng onclick thay vì data-action (không có JS handler)
  - `template/admin_pages/packages/row_parts/download_buttons.php` - legacy list view

### Merge - Security Audit + Performance Pass (v1.8.2 web dev build)

Merge toàn bộ security audit (6 finding), performance (7 fix), CSS minify, và validation từ bản 1.8.2 build ngày 2026-05-20 của web developer. Chi tiết xem mục [1.8.2] bên dưới.

### Compat

- **Tested up to:** WordPress 7.0 (PHP 8.2.29)

---

## [1.8.2] - 2026-05-20 (Security Audit Pass · Performance Pass · CSS Minify)

### Security audit pass - 6 finding

Audit hai tier (security + performance) trước release. Toàn bộ MUST + SHOULD finding security đã được fix:

**MUST FIX (Tier 2 HIGH):**

- **Path traversal trên upload import** [src/Ajax/FileTransfer/ImportUpload.php:117-126](src/Ajax/FileTransfer/ImportUpload.php#L117-L126) - `$_FILES["file"]["name"]` chỉ qua regex `\.(zip|daf)$` + `sanitizeNSCharsNewlineTabs` (không strip `../`). Admin có thể ghi `.zip` ra ngoài `SL_MIGRATIONS_PATH_IMPORTS`. **Fix:** `basename()` trước khi ghép path.
- **CSRF state-change actions** [slm-modern/ModernPackagesController.php:96-159](slm-modern/ModernPackagesController.php#L96-L159) + [views/packages/main/packages.php:25-58](views/packages/main/packages.php#L25-L58) - `create_from_temp`, `action=stop-build`, `action=clear-messages` chấp nhận GET/POST không nonce → dụ admin click link để force_delete package. **Fix:** thêm `wp_nonce_field('slm_modern_legacy_actions')` vào 3 form (`s2.scan1.base.php` × 2 + `packages.php`) + `wp_verify_nonce` ở cả 2 đường vào (modern controller + legacy view).
- **Password leak vào trace log** - 3 chỗ log plaintext credential:
  - [src/Package/Import/PackageImporter.php:309](src/Package/Import/PackageImporter.php#L309) - archive password → log `(length=N)` thay vì raw
  - [classes/entities/class.package.template.entity.php:251](classes/entities/class.package.template.entity.php#L251) - installer password → tương tự
  - [src/Utils/Crypt/CryptBlowfish.php:189,243](src/Utils/Crypt/CryptBlowfish.php#L189) - log raw `$string` (plaintext/ciphertext) khi encrypt/decrypt fail → chỉ log length + error message

**SHOULD FIX (Tier 3 MEDIUM):**

- **Latent SQLi `find_hash_key`** [classes/package/class.pack.php:3038](classes/package/class.pack.php#L3038) - concat `$hash` trực tiếp vào query. Hiện không có user input nhưng future-proof: chuyển sang `$wpdb->prepare(... %s)`.
- **Self-XSS Recovery widget** [views/tools/recovery/widget/recovery-widget-selector.php:91-93](views/tools/recovery/widget/recovery-widget-selector.php#L91-L93) - `$package['name']` echo unescaped → wrap `esc_html()` + cast `(int)` cho `id`.
- **Privilege-escalation `quick_fix`** [ctrls/class.web.services.php:650-672](ctrls/class.web.services.php#L650-L672) - endpoint cap `CAP_BASIC` cho phép set ANY property của `Global_Entity` qua `$_POST['setup']['global'][key]` → user CAP_BASIC có thể đổi `package_mysqldump_path`, `build_mode`, etc. **Fix:** whitelist 5 key (`ajax_protocol`, `basic_auth_*`) cho phép quick_fix.

### Performance - 7 fix

**Tier 1 ROI cao nhất:**

- **`is_pack_running` 5 query → 1** [ctrls/class.web.services.php:1420-1490](ctrls/class.web.services.php#L1420-L1490) - endpoint poller cũ chạy 5 query riêng (mỗi status range 1 query, fetch full 11-col row blob mỗi lần). Consolidate thành 1 `get_row_by_status()` không filter + partition theo status trong PHP. **Tiết kiệm 4 round-trip DB mỗi tick poll**.
- **Poll interval 2s → adaptive 5s-10s** [assets/js/slm-progress-poller.js](assets/js/slm-progress-poller.js) - đổi từ `setInterval(2000)` sang `setTimeout` chain với progressive backoff: signature từ `status+progress+uploads.percent`, không đổi giữa 2 tick → tăng delay ×1.5 đến max 10s; có chuyển động → reset 5s. **Build 10 phút giảm ~300 tick xuống ~120 tick** (60% fewer requests cho phase static).
- **Cache `scan.json` read** [src/Compat/slm-progress-api.php:165-200](src/Compat/slm-progress-api.php#L165-L200) - file vài MB, được đọc + JSON parse mỗi tick poll. Cache `wp_cache_set` keyed `md5(path:mtime)` namespace `sl-migrations` TTL 600s. Site có Redis (như VPS này) thì hit 100% sau tick đầu. **Tiết kiệm 1 file_get_contents (vài MB) + 1 json_decode mỗi tick**.
- **Bootstrap init gate** [src/Core/Bootstrap.php:80-93](src/Core/Bootstrap.php#L80-L93) - `ControllersManager` + `RESTManager` chỉ instantiate khi context admin/AJAX/cron/CLI/REST. Frontend pageview WordPress không còn class-load + register hooks của plugin. **Frontend admin-area cold pageload giảm vài chục instance class**.
- **Autoloader memoize** [src/Utils/Autoloader.php:139-192](src/Utils/Autoloader.php#L139-L192) - `customLegacyMapping()` được autoload chain gọi hàng nghìn lần/request, build mảng 50 entries mỗi lần. **Memoize static** sau call đầu tiên.
- **`getTableCollationList` + `getTableEngineList` duplicate query** [classes/class.db.php:278-329](classes/class.db.php#L278-L329) - `getTableEngineList` execute query 2 lần (`$wpdb->query()` rồi `$wpdb->get_col($query)`). `getTableCollationList` dùng 2-step query+get_col() vô lý (SELECT trả `num_rows` không dùng). **Cả 2 chuyển sang `$wpdb->get_col($query)` một bước**, tiết kiệm 1 round-trip mỗi chunk table khi backup scan.
- **CSS bundle minify** - 10 file CSS chính (`style.css`, `slm.css`, `slm-overrides.css`, `packages.css`, `import.css`, `info-page.css`, `admin-notifications.css`, `admin-notices.css`, `global_admin_style.css`, `style-ctrl.css`) minify in-place qua `clean-css-cli@5 -O2`. **Tổng giảm 199KB → 145KB (27% nhỏ hơn)** - tiết kiệm bandwidth cho mọi admin page plugin.

### Re-audit pass 2 - fix 5 finding mới

Sau khi apply 13 fix lần 1, re-audit để verify và phát hiện thêm:

- **`is_pack_running` keys mismatch** (release blocker) [ctrls/class.web.services.php:1441,1444](ctrls/class.web.services.php#L1441) - sau khi consolidate 5 query, code push vào `cac_ids` / `err_ids` nhưng array init là `cancel_ids` / `error_ids` → JS client nhận empty array → mất status. Fix: đổi tên về `cancel_ids` / `error_ids`.
- **`quick_fix` whitelist typo** [ctrls/class.web.services.php:653-660](ctrls/class.web.services.php#L653) - viết `ajax_protocol_custom` nhưng property thực là `custom_ajax_url` ([class.global.entity.php:182](classes/entities/class.global.entity.php#L182)) → quick-fix dialog "Custom AJAX URL" silently fail. Fix: đổi sang `custom_ajax_url`.
- **OAuth token plaintext leak vào trace log** [src/Utils/OAuth/TokenEntity.php:108](src/Utils/OAuth/TokenEntity.php#L108) - `wp_json_encode($this)` dump nguyên `access_token` + `refresh_token` plaintext khi refresh fail. Trace log readable bởi user CAP_BASIC qua `wp_ajax_sl_migrations_get_trace_log` → token exposure. Fix: chỉ log `storage_type` + `expired` status.
- **Missing CAP_CREATE check cho `create_from_temp`** [slm-modern/ModernPackagesController.php:107](slm-modern/ModernPackagesController.php#L107) + [views/packages/main/packages.php:33](views/packages/main/packages.php#L33) - nonce OK nhưng cap không check → user CAP_BASIC có thể tạo package từ temp data. Fix: thêm `CapMng::can(CapMng::CAP_CREATE)` gate.
- **`renameInstallerFile` chạy mỗi frontend pageview** [src/Core/Bootstrap.php:75](src/Core/Bootstrap.php#L75) - hook `init` priority 20 chạy `ExpireOptions::getUpdate` (transient lookup + DB write) trên **mọi pageview frontend** dù không cần. Fix: wrap vào `$isContextual` gate cùng với ControllersManager/RESTManager.

### Files changed

**Security:**
- `src/Ajax/FileTransfer/ImportUpload.php` - basename path traversal
- `slm-modern/ModernPackagesController.php` - CSRF nonce + CAP_CREATE
- `views/packages/main/packages.php` - CSRF nonce + CAP_CREATE
- `views/packages/main/s2.scan1.base.php` - wp_nonce_field
- `src/Package/Import/PackageImporter.php` - không log archive password
- `classes/entities/class.package.template.entity.php` - không log installer password
- `src/Utils/Crypt/CryptBlowfish.php` - không log plaintext khi crypt fail
- `src/Utils/OAuth/TokenEntity.php` - không dump OAuth token plaintext
- `classes/package/class.pack.php` - `find_hash_key` prepare
- `views/tools/recovery/widget/recovery-widget-selector.php` - esc_html package name
- `ctrls/class.web.services.php` - quick_fix whitelist + custom_ajax_url

**Performance:**
- `ctrls/class.web.services.php` - `is_pack_running` 1 query + đúng response keys
- `assets/js/slm-progress-poller.js` - adaptive poll backoff
- `src/Compat/slm-progress-api.php` - cache scan.json
- `src/Core/Bootstrap.php` - context gate + renameInstallerFile
- `src/Utils/Autoloader.php` - memoize mapping
- `classes/class.db.php` - bỏ duplicate query
- 10 file `assets/css/*.css` minified (27% smaller)

---

## [1.8.1] - 2026-05-19 (Encryption Key Hotfix · Legacy DupPro Cleanup · Footer)

### Fix nghiêm trọng - mất SFTP credentials & reset Default storage retention sau mỗi lần update

**Triệu chứng:** Sau khi update plugin, một số web bị mất hoàn toàn credentials SFTP (server/username/password trống), và **mọi web** đều bị reset Default storage: `max_packages` từ giá trị bạn đã đặt (vd 1) thành `20`, và option "tự xoá gói cũ" (`purge_packages`) bị bỏ check.

**Root cause:** [src/Core/Upgrade/UpgradeFunctions.php:258-265](src/Core/Upgrade/UpgradeFunctions.php#L258-L265) - hàm `storeDupSecureKey()` so `$currentVersion` với `'4.5.0'` (di sản Duplicator Pro upstream). Plugin đã rebrand sang scheme **1.x.x** → `version_compare('1.x.x', '4.5.0', '<=')` **luôn TRUE** → mỗi lần update rơi vào nhánh:

```php
CryptBlowfish::createWpConfigSecureKey(true, true); // OVERWRITE=true, legacy MD5
```

→ Ghi đè `SL_MIGRATIONS_AUTH_KEY` trong wp-config bằng MD5 hash legacy → toàn bộ config đã encrypt trong DB (`SLM_PRO_Storage_Entity.config`) decrypt fail → [AbstractStorageEntity::__wakeup](src/Models/Storages/AbstractStorageEntity.php#L134-L156) fallback về `getDefaultConfig()`:

- **Default storage** ([Local/DefaultLocalStorage.php:49-50](src/Models/Storages/Local/DefaultLocalStorage.php#L49-L50)): `max_packages=20`, `purge_packages=false` ← site nào cũng bị vì site nào cũng có Default storage entity.
- **SFTP storage** ([addons/ftpaddon/src/Models/SFTPStorage.php:37-54](addons/ftpaddon/src/Models/SFTPStorage.php#L37-L54)): `password=''`, `username=''`, `server=''` ← chỉ site có cấu hình SFTP từ trước mới bị.

**Fix:** Gỡ **toàn bộ** legacy migration của Duplicator Pro 4.x khỏi [src/Core/Upgrade/UpgradeFunctions.php](src/Core/Upgrade/UpgradeFunctions.php) - plugin đã standalone, không còn user nào chạy upstream:

- `storeDupSecureKey()` - bỏ nhánh overwrite legacy MD5, chỉ ensure key tồn tại
- `updateStorages()` - gỡ hẳn (gọi `updateFromGlobal()` reset `max_packages=20`, `purge_packages=false` mỗi lần update → **đây là root cause của reset Default storage trên mọi web**)
- `updateTemplates()`, `updatePackageComponents()`, `initEmailSummaryRecipients()` - gỡ hẳn (cùng pattern bug version-compare 4.5.x)
- `renameLegacyTablesName()` - gỡ hẳn (rename table `sl_migrations_*` → tên mới, fork 1.x đã dùng tên mới ngay từ đầu)
- `moveDataToSecureGlobal()`, `moveStorageSettingsToDynamicEntity()` - gỡ hẳn (migrate global → secure global / dynamic entity, đã chạy xong từ lâu)
- Constants `LAST_VERSION_*`, `FIRST_VERSION_WITH_NEW_TABLES`, `LEGACY_DB_*` - xoá
- [DefaultLocalStorage::updateFromGlobal()](src/Models/Storages/Local/DefaultLocalStorage.php) - xoá luôn (orphan, không còn ai gọi)

`performUpgrade()` giờ chỉ làm 2 việc: init DB schema + ensure `SL_MIGRATIONS_AUTH_KEY` tồn tại. Mỗi lần update plugin không còn đụng vào encrypted config trong DB.

### Footer modern UI căn từ mép trái

Trước đây footer trên các trang `page_sl-migrations` có `left: 160px` (khi sidebar expanded) / `left: 36px` (khi folded) + media query mobile `left: 0`. Đổi thành cố định `left: 0` cho mọi trạng thái sidebar → footer full-width nhất quán.

### Security - Handlebars 4.0.10 → 4.7.8

Bundle `assets/js/handlebars.min.js` đang ở **4.0.10** - dính CVE-2019-19919 (RCE qua prototype-pollution trong template compiler) và một số RCE/XSS chain khác trên 4.0.x-4.4.x. Upstream Duplicator Pro đã upgrade trong 4.5.25 với note "JavaScript security vulnerabilities (Handlebars RCE/prototype-pollution)".

Replace bằng [Handlebars 4.7.8](https://github.com/handlebars-lang/handlebars.js/releases/tag/v4.7.8) (bản patch security cuối của dòng 4.x). API `registerHelper` + `compile` đang dùng ở [assets/js/javascript.php](assets/js/javascript.php), [views/packages/main/s2.scan3.archive.php](views/packages/main/s2.scan3.archive.php), [views/tools/diagnostics/inc.validator.php](views/tools/diagnostics/inc.validator.php) backward-compatible.

### Storage form validation - chống crash storage list

Trước đây nếu user nhấn Save Provider với field server / username trống, storage entity vẫn được save với config rỗng → trang Storage list sau đó render `getLocationString()` trên config trống → 500 / unusable page (cùng triệu chứng [DupPro 4.5.24.4](https://snapcreek.com/duplicator/changelog) fix).

Validate server-side trong [FTPStorage::updateFromHttpRequest](addons/ftpaddon/src/Models/FTPStorage.php), [SFTPStorage::updateFromHttpRequest](addons/ftpaddon/src/Models/SFTPStorage.php):

- FTP: `server` + `username` không được trống
- SFTP: `server` + `username` không được trống, **và** phải có ít nhất 1 trong `password` / `private_key` (đã lưu sẵn hoặc vừa nhập)

Block save + trả message tiếng Việt rõ ràng nếu thiếu.

### Defensive fixes

- **MySQL NULL table stats** ([class.pack.database.php:229](classes/package/class.pack.database.php#L229)) - `DATA_LENGTH + INDEX_LENGTH` trên InnoDB có thể trả NULL khi statistics chưa được tính → cộng NULL ra NULL → fatal khi cast int trong scan loop. Bọc `COALESCE(..., 0)` cho cả 2 cột.
- **Trace log download `fpassthru` disabled** ([ctrls/class.web.services.php:1058](ctrls/class.web.services.php#L1058)) - fpassthru bị disable trên một số shared host qua `disable_functions`. Fallback đọc theo chunk 8KB nếu fpassthru không tồn tại hoặc trả `false`.

### Files changed
- `assets/js/handlebars.min.js` - Handlebars 4.7.8 (security)
- `addons/ftpaddon/src/Models/FTPStorage.php` - validate server + username
- `addons/ftpaddon/src/Models/SFTPStorage.php` - validate server + username + (password OR private_key)
- `classes/package/class.pack.database.php` - COALESCE NULL table stats
- `ctrls/class.web.services.php` - fallback chunk read khi fpassthru disabled
- `src/Core/Upgrade/UpgradeFunctions.php` - rewrite gọn còn 3 method (`performUpgrade`, `updateDatabase`, `storeDupSecureKey`)
- `src/Core/MigrationMng.php` - call `updateDatabase()` không param
- `src/Models/Storages/Local/DefaultLocalStorage.php` - xoá `updateFromGlobal()` orphan
- `slm-modern/modern.php` - đơn giản hoá inline critical CSS footer

---

## [1.8.0] - 2026-05-18 (Modern Recovery Point UI · Modal Escape Fix · Import Min Version)

### Recovery Point UI hoàn chỉnh trong Modern UI

Trước đây Set Recovery Point chỉ có ở menu Hamburger của UI legacy → modern UI thiếu hẳn action. Phiên bản này hoàn thiện trải nghiệm:

**Backend AJAX endpoints (cùng nonce `slm_modern`):**
- `slm_modern_recovery_status` - đọc state hiện tại (read-only)
- `slm_modern_recovery_set` - wrap `RecoveryPackage::setRecoveablePackage()`
- `slm_modern_recovery_reset` - wrap `SLM_PRO_CTRL_recovery::actionResetRecoveryPoint()`

**Frontend:**
- **Tools → Recovery tab:** panel modern "Recovery Point" ABOVE legacy với status dot live, box "Recovery Point hiện tại" (tên + link khôi phục + nút Reset), dropdown chọn package + nút "Set as Recovery Point"
- **Packages list:** icon shield trong row actions (chỉ hiện khi `isPackageIdRecoveable()` = true và package chưa phải current Recovery)
- **Package detail page:** nút "Set as Recovery Point" trong "Hành động"; badge "Recovery Point" cạnh status pill khi đã set

**Visual identity:**
- Badge "Recovery Point" gradient xanh dương `#3b82f6 → #1d4ed8` với box-shadow ring
- Row trong Packages list được highlight với `inset 3px 0 0 #2563eb` (border trái xanh) + gradient background nhẹ
- Status dot 3 trạng thái: xanh dương (set) / cam (unset) / đỏ (error)
- Heading "Recovery Point" (modern) + "Disaster Recovery" (legacy) sync cùng `font-size: 18px, weight: 700, icon 20×20`; icon shield xanh dương, icon đám cháy xanh lá

### Bảo vệ Recovery Point khỏi mất dữ liệu

Trước đây cron retention `AbstractStorageEntity::purgeOldPackages()` chỉ sort theo modified time + cắt N gói cũ nhất theo `max_packages`. Nếu gói đang là Recovery Point cũ hơn các gói mới → bị **xoá vĩnh viễn** → URL khôi phục hỏng → mất khả năng khôi phục khi cần.

**Fix:** `purgeOldPackages()` lookup `RecoveryPackage::getRecoverPackageId()` → load Package object → `archive_name` → so khớp với từng file trong loop. File khớp được **skip** (log `"Skipping purge: package is current Recovery Point"`).

Đồng thời block xoá thủ công:
- **Client-side:** nút Delete trong row + detail bị `disabled` với tooltip "Không thể xoá - đang là Recovery Point. Reset Recovery Point trước."
- **Server-side guard:** `ajaxDelete` reject HTTP 409 + message tiếng Việt nếu request có ID là current Recovery Point (chặn bypass UI).

### Fix `IMPORT_ENABLE_MIN_VERSION` quá cao → mọi package bị reject

`PackageImporter::IMPORT_ENABLE_MIN_VERSION` cố định `'4.0.0'` (di sản từ Duplicator Pro upstream). sl-migrations rebrand sang scheme `1.x` → `version_compare('1.7.x', '4.0.0', '<') = true` → `RecoveryStatus::isRecoveable()` reject **mọi package** → `getRecoverablesPackages()` luôn trả về mảng rỗng → không bao giờ set được Recovery Point.

Hạ ngưỡng xuống `'1.0.0'` (cùng `IMPORT_SUB_SITE_IN_MULTISITE_MIN_VERSION` và `IMPORT_BRIDGE_MIN_VERSION`).

### Fix Dashboard widget link 404 "Gói không tồn tại"

Click tên package trong widget Dashboard → trang detail báo "Gói không tồn tại". Root cause: `PackagesPageController::getPackageDetailsUrl()` build URL với key `id`, nhưng `slm-modern/views/detail.php` chỉ đọc `$_GET['package_id']`.

**Fix:** detail.php accept cả 2 keys - ưu tiên `package_id` (modern), fallback `id` (legacy widget).

### Fix modal `&quot;` thô (UI global)

`SLM.modal.confirm({body: ...})` hiển thị `&quot;` literal thay vì dấu nháy do double-escape: `escapeHtml()` chuyển `"` → `&quot;`, sau đó wrap thành **textNode** (browser không decode entity trong textNode).

**Fix:** `assets/js/slm-ui.js` dòng 73 dùng `el('div', {text: opts.body})` (textContent path) thay vì `escapeHtml(body)` wrap textNode. Bug này ảnh hưởng MỌI modal trong plugin, không chỉ Recovery.

### Cache version sync

`SLM_MODERN_VER` đổi từ hardcode `'1.7.3'` sang `SL_MIGRATIONS_VERSION` → mỗi lần bump plugin version, modern.css + modern.js auto cache-bust trong browser. Trước đây user phải hard refresh hoặc đợi browser cache TTL.

### Files changed
- `slm-modern/`: `ModernPackagesController.php`, `ModernToolsController.php`, `views/_row.php`, `views/detail.php`, `modern.js`, `modern.css`, `modern.php`
- `src/Models/Storages/AbstractStorageEntity.php` - purge skip Recovery Point
- `src/Package/Import/PackageImporter.php` - lower `IMPORT_*_MIN_VERSION`
- `assets/js/slm-ui.js` - fix modal escape bug

---

## [1.7.4] - 2026-05-18 (Dashboard Widget Link Fix · Modern Recovery Set/Reset)

### Fix - Dashboard widget "Recently Packages" link 404

**Triệu chứng:** Click tên package trong widget Dashboard → trang detail báo "Gói không tồn tại - Có thể gói đã bị xóa hoặc ID không hợp lệ".

**Root cause:** Mismatch query key giữa URL generator legacy (`getPackageDetailsUrl()` truyền `id={ID}`) và modern detail view (`detail.php` chỉ đọc `$_GET['package_id']`). ID = 0 → `SLM_PRO_Package::get_by_id(0)` trả về null → "Không tìm thấy".

**Fix:** `slm-modern/views/detail.php` đọc cả 2 keys - ưu tiên `package_id` (modern), fallback `id` (legacy widget + các nơi khác chưa migrate sang URL pattern mới).

### Modern Recovery Point - set/reset từ UI mới

Trước đây tính năng "Set Recovery Point" chỉ truy cập được qua **menu Hamburger** ở row package trong UI legacy. Modern UI hoàn toàn thiếu action này → user phải vào tab Recovery của Tools, đọc hướng dẫn, rồi sang page Packages cũ để thao tác.

**Bổ sung:**

1. **Nút shield trong row actions (Packages list - modern)** - chỉ hiện khi package đủ điều kiện (có cả Archive + Installer). Click → modal confirm → AJAX set → toast.

2. **Panel "Recovery Point" trong tab Tools → Recovery (modern)** - render ABOVE legacy content, gồm:
   - Status dot (đã/chưa thiết lập) tự load qua `slm_modern_recovery_status`
   - Khối "Recovery Point hiện tại" + link khôi phục + nút Reset (hiện khi đã set)
   - Dropdown chọn package + nút "Set as Recovery Point"
   - Empty state nếu chưa có package đủ điều kiện

**Backend wrapper** trong `ModernPackagesController` (file đã có nonce pattern `slm_modern`):
- `wp_ajax_slm_modern_recovery_status` - đọc state, không destructive
- `wp_ajax_slm_modern_recovery_set` - wrap `RecoveryPackage::setRecoveablePackage()`
- `wp_ajax_slm_modern_recovery_reset` - wrap `SLM_PRO_CTRL_recovery::actionResetRecoveryPoint()`

Tất cả endpoints dùng cùng 1 nonce `slm_modern` đã có sẵn - không cần thêm nonce key mới ra global config.

---

## [1.7.3] - 2026-05-18 (Timezone Fix · Sticky Footer · Plugin Row Meta · Landing Page)

### Fixes
- **Timezone hiển thị sai trên Packages list**: cột `Created` show giờ UTC thay vì giờ Việt Nam (vd schedule chạy 12:00 ICT nhưng hiển thị 05:00). Root cause: dùng `date_i18n()` - legacy API coi timestamp đã pre-offset, hiển thị raw UTC. Fix: chuyển sang `wp_date()` (modern API tôn trọng WP timezone) + parse string với suffix `UTC` để strtotime hiểu đúng. ([ModernPackagesController.php:217-230](slm-modern/ModernPackagesController.php))
- **Sticky footer + FOUC trên Packages/Schedules/Storage/Tools**: nhiều iteration để đạt:
  - Footer pin xuống đáy viewport trên trang content ngắn
  - Không scroll thừa
  - Không flash layout (logo "nhảy" từ giữa lên top khi CSS load)
  - Footer căn lề đúng với mép phải sidebar (160px default / 36px folded / 0 mobile)
  - Back-to-top button (#backtotop của plugin wp-extra) nổi trên dải footer thay vì bị che
  
  Approach cuối: `position: fixed` cho `#wpfooter` (inline vào `<head>` qua hook `admin_head` priority 1) + `padding-bottom: 60px` cho `#wpbody-content`. Bỏ approach flex trên `#wpwrap` vì trong khi WP core CSS chưa load, sidebar #adminmenuwrap trở thành flex item in-flow → đẩy content xuống dưới → flash.

### Enhancements
- **Plugin row meta links** ([Bootstrap.php metaLinks](src/Core/Bootstrap.php#L659)): thêm 2 link **README** + **Changelog** vào dòng meta dưới row plugin (kế bên "Phiên bản · Bởi Steven Lam · Ghé thăm trang web · Help"). Tất cả trỏ về tài liệu trên phatdev.net (`target="_blank"`):
  - README → `phatdev.net/docs/sl-migrations/getting-started`
  - Changelog → `phatdev.net/docs/sl-migrations/changelog` (article được seed từ file CHANGELOG.md)
  - Help → `phatdev.net/docs/sl-migrations`
  - Ghé thăm trang web → `phatdev.net/sl-migrations` (landing page)
- **Tools → Support tab**: thêm card thứ 3 "Tài liệu plugin" với link README + Changelog (target blank, mở phatdev.net trong tab mới).
- **Landing page mới**: `/sl-migrations` trên phatdev.net - marketing page hoàn chỉnh chuẩn sale-ready cho lúc mở bán plugin sau này. Gồm hero (gradient text + CTA), 6 features card, 3 use case (Agency/DevOps/Production), tech specs, pricing section (Free internal), bottom CTA, footer.
- **Doc seed script mới** ([seed-docs-sl-migrations-changelog.mjs](../../phatdev.net/app/scripts/seed-docs-sl-migrations-changelog.mjs)): idempotent script đẩy nội dung CHANGELOG.md vào DB docs của phatdev.net (table `docs_articles`), auto-extract version + date mới nhất.

### Technical
- `SL_MIGRATIONS_VERSION`, `SLM_ASSETS_VER`, `SLM_MODERN_VER` → `1.7.3`
- Plugin URI đổi từ `https://phatdev.net/` → `https://phatdev.net/sl-migrations`
- Inline critical CSS trong `<head>` qua `admin_head` priority 1 - không thêm HTTP request, áp dụng từ frame đầu tiên

---

## [1.7.2] - 2026-05-18 (Run Now Redirect · Import Settings Hardening · Layout & Docs)

### Fixes
- **Layout - sticky footer**: trên các trang Packages/Schedules/Storage/Tools khi nội dung ngắn, `#wpbody-content` bị ép `min-height: calc(100vh - 100px)` cộng thêm `#wpadminbar` (32px) + `#wpfooter` (~52px) → tổng vượt 100vh → scroll dọc thừa, footer bị đẩy dưới viewport. Fix: dùng flex column trên `#wpbody` (`min-height: calc(100vh - 32px)` desktop / 46px mobile) với `flex: 1 0 auto` cho content và `flex: 0 0 auto` cho footer → footer luôn pin xuống đáy viewport, content tự grow lấp khoảng giữa, không sinh scroll.
- **Back-to-top button overlap**: `#backtotop` của plugin `wp-extra` được inject inline `bottom: 15px`, sau khi pin footer xuống đáy thì nút này bị footer che. Fix: override `bottom: 70px !important` chỉ trong scope `body[class*="page_sl-migrations"]` để nút float ngay trên dải footer.
- **Schedules - Run Now**: dialog kẹt ở "Running Schedule, Please Wait..." và không tự nhảy qua tab Packages khi click. Root cause: `.done()` không fire trong tình huống PHP-FPM/Nginx connection reset (HTTP/2 reuse). Fix: chuyển sang fire-and-forget pattern - dispatch AJAX rồi redirect sau 300ms, browser vẫn gửi xong request trước khi navigate. Bỏ luôn `.always()` + safety-timer dài 12s ở fix cũ.
- **Import Settings - cross-site `.dup` files**: import file `.dup` từ Duplicator Pro / SL-Migrations site khác có thể bị mất config khi storage là subclass (SFTP, S3, ...) vì `ReflectionClass(self::class)` chỉ lấy property của `AbstractStorageEntity`, bỏ sót property của subclass. Fix: dùng `static::class` để late-static-binding lấy đúng class runtime.
- **Import Settings - robustness**: `MigrateSettings::importStorages/Templates/Schedules/Settings` không có try/catch, 1 entity malformed kéo đổ toàn bộ import. Fix: bọc từng item trong try/catch + validate `is_array($data)` + check `isset($data['id'])` trước khi map id.
- **`AbstractStorageEntity::__wakeup()`**: `isset(null[$key])` ngầm khi `CryptBlowfish::decrypt()` thất bại - silently bỏ qua, config về default mà không log. Fix: explicit `is_array($config)` guard + fallback `[]`.
- **`AbstractStorageEntity::settingsImport()`**: validate `$data` là array, ép `config` luôn là array (nếu source export sai cấu trúc), try/catch quanh `setValue()` để 1 prop lỗi không phá entity.

### Enhancements
- **`MigrateSettings::decodeImportPayload()`** helper mới: thử decrypt format chuẩn (`CryptCustom('test')` - tương thích với Duplicator Pro export); nếu fail thì fallback sang plain JSON. Nâng tỷ lệ đọc được file `.dup` từ các phiên bản / nguồn khác nhau.

### Technical
- `SL_MIGRATIONS_VERSION`, `SLM_ASSETS_VER` → `1.7.2`
- Files changed: `template/admin_pages/schedules/schedule_list.php`, `src/Utils/Settings/MigrateSettings.php`, `src/Models/Storages/AbstractStorageEntity.php`

### Verified
- Smoke test trên 2 file `.dup` thật (dpro-export từ site reviewlaptop.vn và tandoanh.vn): decrypt OK, parse OK, SFTP credentials đầy đủ (server / port / username / password / private_key / timeout / chunking) được import qua subclass `SFTPStorage`.

---

## [1.7.1] - 2026-05-17 (CSS Fixes · Template Edit Page)

### Fixes
- **Template Edit - Filters section**: `.section-title { height: 20px; padding: 10px }` với WP admin `box-sizing: border-box` tạo content area = 0px → checkbox "Enable" bị clip, filter-links bị overlap. Fix: override `height: auto; min-height: 44px; align-items: center`.
- **Template Edit - Installer → Branding row**: `<select>` có `min-height: 40px` trong khi nút "Preview" có `min-height: 38px` → chiều cao không khớp. Fix: đồng bộ cả hai về `height: 38px`.
- **Filter textareas**: `slm-overrides.css` global `textarea { min-height: 100px }` khiến `#filter-paths` và `#filter-exts` quá cao gây layout overflow. Fix: scope riêng `min-height: 60px; height: 80px`.

### Technical
- `SLM_ASSETS_VER`, `SLM_MODERN_VER`, `SL_MIGRATIONS_VERSION` → `1.7.1`
- CSS fixes trong `assets/css/slm-overrides.css`

---

## [1.7.0] - 2026-05-14 (Language Policy · Modern UI · Table Layouts)

### Chính sách ngôn ngữ
- **Short labels / tab / button** chuyển hết về English (Packages, Schedules, Storage, Tools, Settings, Info, Apply, Save Schedule, Activated, Refresh, Reset, ...)
- **Vietnamese** chỉ giữ cho mô tả dài, tooltip, error message, warning, confirmation prompt
- Thống nhất cách xưng hô trong mọi chuỗi giao diện: chỉ dùng "**bạn**"
- `translations-vi.php` gọn từ ~996 dòng còn ~110 dòng, giữ 40 entries giải thích dài

### UI Redesign
- **Modern UI hero titles** toàn bộ về English: Packages / Import / Schedules / Storage / Tools / Settings
- **Disaster Recovery** card layout hoàn chỉnh: header đỏ khi chưa thiết lập, xanh lá khi đã set, icon house-fire trong vòng tròn, mô tả dùng "bạn"
- **Storage table**: cột chia 10% / 30% / 60% (checkbox / Name / Type)
- **Schedule table**: checkbox 10% + 6 cột còn lại 15% mỗi cột (90÷6)
- **Packages table**: 10% checkbox + 22% Name + 14% Status + **18% Download** + 16% Created + 10% Size + 5% Restore + 5% Details
- **Modern UI Packages**: thêm cột Download với 3 nút icon (Installer / Archive / Both Files) + fix Created field hiển thị đúng `d/m/Y H:i`
- Table styling đồng bộ trên cả 3 list tables: header xám nhẹ + body trắng + alternate `#fafbfd` + hover xanh nhạt
- **"No Schedules Found / Create New Schedule"** canh giữa thay vì bám trái
- **Bulk Actions select** đồng bộ chiều cao 44px với nút Apply / Add New (max-height + zero vertical padding để override browser default `<select>` rendering)
- **Trace log widget** vị trí chính xác `right: 60px; bottom: 12px`, 4 nút (View / Clear / Download / Turn Off) trên 1 hàng

### Quality of Life
- Ẩn panel "Screen Options" trên tab Packages (modern UI không có column toggle nên panel đó chỉ làm rối)
- Form wrapper `#dup-storage-form`, `#form-sl_migrations`, `#dup-schedule-form` ép `display: block; width: 100%` để table stretch full card width
- Table `table-layout: fixed` + `width: 100%` ở 3 list tables → column % được honored chính xác
- Hidden columns (`dup-note-column`, `dup-storages-column`, `dup-age-column`) `display: none` để table không bị "vướng"

### Technical
- `SLM_ASSETS_VER` → `1.7.0`, `SLM_MODERN_VER` → `1.7.0` (đồng bộ với plugin version)
- `ModernPackagesController::normalisePackage()` ưu tiên `getCreated()` method, thêm 6 download fields (installer_url, archive_url, installer_exists, archive_exists, archive_filename, archive_format)
- `add_filter('screen_options_show_screen', ...)` ẩn screen options trên `toplevel_page_sl-migrations`

---

## [1.6.0] - 2026-05-14 (Info Tab · Disaster Recovery · UI Polish)

### Thêm mới
- **Tab Thông tin (Info)**: Trang giới thiệu plugin với hero gradient xanh, 6 cards tính năng nổi bật, parser CHANGELOG.md tự động, bảng thông tin hệ thống (WP/PHP/MariaDB version, bộ nhớ, múi giờ, version plugin)
- **3 nút download riêng biệt** trên packages list: Tải PHP Installer · Tải Archive (DAF/ZIP) · Tải cả 2 file (thay cho dropdown menu)
- Cột Tải về (Download) di chuyển ngay sau cột Trạng thái thay vì cuối bảng

### UI Redesign
- **Disaster Recovery** wrap thành card trắng bo tròn, header đỏ khi chưa thiết lập / xanh lá khi đã set, icon house-fire trong vòng tròn
- **Spinner ajax-loader** đổi từ logo Duplicator xoay sang icon vòng tròn Font Awesome (`fa-circle-notch fa-spin`)
- **Đồng bộ style nút**: plain `.button`, `.button.action`, `.button-primary`, `.button-secondary` đều đi theo style "big pill" 9px×18px, min-height 38px
- **Compact trace log widget**: nút Xem/Xóa/Tải/Tắt thu nhỏ vừa vặn trong card, padding container thoáng hơn
- **Storage list table** cột checkbox thu gọn 40px (trước đó bị rule `td:first-child{width:240px}` của bảng Server Info ăn vào, đẩy cột Name xa lề trái)
- **Schedule edit form**: select Package Template và 3 nút icon (↻) đều cao 36px, select Start Time `00:00` canh giữa vertical
- Ẩn cột Note + Storages + Age trên packages list để Trạng thái sát Tên gói

### Dịch tiếng Việt
- Thêm ~50 chuỗi dịch: Disaster Recovery (Khôi phục sự cố, Tuổi sao lưu, Bản khôi phục sự cố...), Schedule form (Kích hoạt, Bật lịch trình này, Lưu lịch trình, Mẫu gói sao lưu, Thời gian bắt đầu...), Packages download buttons
- Refactor multi-line `_e()` calls thành inline Vietnamese để tránh whitespace mismatch trong gettext overlay

### Branding
- **Đổi Author header**: `Phat Lam` → `Steven Lam` trên sl-migrations.php và README.md
- **Clean legacy copyright**: 371 file rewrite từ `@copyright (c) 2022,` → `@copyright (c) 2022-2026, Steven Lam`
- **Loại bỏ Snap Creek refs**: thay `@author <admin@snapcreek.com>` → `@author Steven Lam <info@phatdev.net>`, đổi `snapcreek.com` → `phatdev.net` trên toàn bộ installer + library files

### Technical
- `SLM_ASSETS_VER` bumped to `1.6.0` để bust cache trình duyệt
- Mới: `src/Controllers/InfoPageController.php` (menuPos=90, CAP_BASIC) · `views/info/main.php` · `assets/css/info-page.css`
- Mới: `INFO_SUBMENU_SLUG = 'sl-migrations-info'` trong `ControllersManager`

---

## [1.0.0] - 2026-05-13 (Initial Release)

### Phát hành bản đầu tiên
- Sao lưu toàn site WordPress (files + database) thành một archive duy nhất kèm installer PHP
- Khôi phục 1-click trên bất kỳ host WordPress tương thích nào
- Sao lưu định kỳ theo lịch (hàng giờ/ngày/tuần/tháng)
- Lưu trữ remote qua **FTP / SFTP** an toàn
- Lưu trữ local mặc định tại `wp-content/backups-slm/`
- Plugin namespace `SLMigrations\` (PSR-4) với composer-scoped vendor namespace `VendorSLMigrations\`
- Constants `SL_MIGRATIONS_*` / `SLM_PRO_*`; hooks `sl_migrations_*`; text domain `sl-migrations`
- Installer engine tại `installer/slm-installer/`

### Security
- Không có lưu lượng mạng ra ngoài trong quá trình vận hành bình thường
- Không kích hoạt license, không telemetry, không kiểm tra cập nhật từ xa
- Plugin `Update URI: false` - WordPress sẽ không gọi về máy chủ để cập nhật

### Branding
- Áp dụng design system TanDoanh Plugins (bảng màu xanh-trung tính, card bo tròn, shadow nhẹ)
- Logo và admin CSS overrides đi kèm plugin

### Notes
- Chỉ hỗ trợ WordPress single-site (`Network: false`)
- Cloud storage (Google Drive / OneDrive / Dropbox / S3) chưa có trong bản này
- Sẽ hỗ trợ cloud trong tương lai qua self-hosted OAuth flow (operator tự cung cấp Client ID / Secret)
