# Tài liệu bàn giao và bảo trì

Dành cho người quản trị hệ thống của Mediworld và bất kỳ đơn vị nào tiếp nhận bảo trì sau này.
Tài liệu triển khai lên HawkHost nằm riêng ở `HUONG-DAN-TRIEN-KHAI-HAWKHOST.md`; ghi chú kỹ thuật cho lập trình viên ở `CLAUDE.md`.

## 1. Quyền sở hữu và quyền sử dụng mã nguồn

- Toàn bộ mã nguồn của hệ thống (thư mục `src/`, `drizzle/`, `scripts/`, các tệp cấu hình và tài liệu ở thư mục gốc) là sản phẩm viết riêng cho Mediworld, không dựa trên mã nguồn đóng của bên thứ ba. Kho mã nằm ở một kho Git riêng tư; toàn bộ lịch sử thay đổi có trong Git.
- Hệ thống chỉ dùng thư viện mã nguồn mở với giấy phép cho phép dùng thương mại và sửa đổi (chủ yếu MIT/ISC/Apache-2.0). Không có thư viện nào buộc phải công khai mã nguồn của hệ thống. Danh sách đầy đủ 186 gói chạy thật, kèm giấy phép, ở `THIRD-PARTY-LICENSES.md`.
- Không có khoá bản quyền, không gọi về máy chủ của bên phát triển, không có "cửa hậu": ứng dụng chỉ nói chuyện với cơ sở dữ liệu và (nếu bật) máy chủ email/Zalo do chính Mediworld cấu hình.
- **Việc chuyển giao quyền tác giả và quyền sở hữu là điều khoản pháp lý của hợp đồng**, không phải thứ mã nguồn tự chứng minh. Tài liệu này chỉ ghi lại các sự thật kỹ thuật để đối chiếu với hợp đồng.
- Khi bàn giao, cần chuyển đủ: (1) kho Git đầy đủ lịch sử; (2) tệp `.env.local` của máy chủ thật (qua kênh an toàn, không gửi qua chat/email thường); (3) bản sao lưu cơ sở dữ liệu và thư mục tệp tải lên; (4) tài khoản quản trị của hosting và tên miền.

## 2. Kiến trúc và chi phí hạ tầng

| Thành phần | Chọn | Ghi chú |
|---|---|---|
| Ứng dụng | Next.js 15 + React 19, chạy bằng Node.js 20 trở lên (máy chủ HawkHost đang dùng Node 22) | một tiến trình Node duy nhất, không cần Redis, hàng đợi hay dịch vụ ngoài |
| Cơ sở dữ liệu | PostgreSQL 10 trở lên | bản demo trên máy lập trình dùng PostgreSQL nhúng (PGlite), cùng một mã |
| Tệp tải lên | Thư mục trên đĩa (`crm-uploads`) | chỉ phục vụ qua đường dẫn có kiểm tra đăng nhập và phạm vi |
| Tác vụ nền | 1 lệnh chạy mỗi đêm (`npm run jobs:nightly`) | phân hạng đại lý, KPI, cảnh báo, dự báo; xử lý 40.000 đơn trong ~2,3 giây |

**Cấu hình chi phí thấp đã kiểm chứng:** gói hosting Node.js dùng chung là đủ cho hàng chục người dùng đồng thời. Số kết nối tối đa tới PostgreSQL chỉnh bằng biến `PG_POOL_MAX` (mặc định 10; hạ xuống 3–5 nếu nhà cung cấp giới hạn kết nối).

**Kiểm thử tải (27/09/2026, bản build chạy thật, PGlite — chậm hơn PostgreSQL thật):** với 3.077 khách, 40.603 đơn, 98.459 dòng hàng, 24.397 phiếu thu, 150.124 dòng nhật ký, hầu hết các trang phản hồi dưới 150 ms; trang chậm nhất là danh sách khách hàng (~0,65 giây với 3.000 khách) và xuất Excel toàn bộ khách (~0,8 giây). Lần mở đầu tiên sau khi khởi động có thể chậm hơn (~1 giây). Trang chỉ tải theo phạm vi và có phân trang trong câu lệnh SQL nên thời gian tăng gần như tuyến tính theo số bản ghi.

## 3. Những gì tùy biến được mà không cần lập trình

Người có vai trò Ban giám đốc chỉnh trực tiếp trên giao diện:

- **Danh mục:** sản phẩm, nhóm sản phẩm, kênh bán, khu vực (miền → tỉnh → quận → phường), phòng ban / team (cây nhiều cấp, trưởng phòng), chương trình bán hàng, khoá chuyển giao, nhập Excel hàng loạt.
- **Cài đặt chung:** ngưỡng sụt giảm doanh thu (mặc định 30%), ngưỡng hoàn thành chỉ tiêu nhân sự (mặc định 70%), thông tin công ty.
- **Người dùng và phân quyền:** vai trò (Ban giám đốc / Giám đốc khu vực / Quản lý / Nhân viên), phòng ban, khu vực, các cờ quyền lẻ (đăng văn bản, đăng thông báo, sửa thông tin nhân viên). Người dùng chỉ thấy dữ liệu trong nhánh của mình.
- **Chỉ tiêu:** giao cho công ty, khu vực, phòng ban, nhân viên theo tháng/quý/năm; KPI tự tạo; sự kiện mời khách có chỉ tiêu.
- **Nội dung:** văn bản công ty, thông báo trang chủ.

Những thứ cần chỉnh mã (rất ít dòng, đã tách ra một chỗ):

| Muốn đổi | Sửa ở |
|---|---|
| Các loại phiếu đề xuất và trường riêng | `src/lib/proposal-types.ts` |
| Các loại văn bản | `src/lib/document-categories.ts` |
| Danh sách loại lớp chuyển giao gợi ý | `src/lib/event-kinds.ts` |
| Ngưỡng phân hạng đại lý A–E, số tháng | đầu tệp `src/lib/classification.ts` |
| Nhãn tiếng Việt của trạng thái, loại cảnh báo | `src/lib/format.ts` |
| Ba mức cảnh báo "không lấy hàng" (2/3/6 tháng) | `INACTIVE_THRESHOLDS` ở `src/lib/alerts.ts` |

⚠️ **Riêng dòng đầu (phiếu đề xuất) cần lưu ý khi bàn giao:** phần Đề xuất hiện build theo hướng 4 loại phiếu cố định + duyệt tuần tự qua TGĐ (chi tiết ở `DAC-TA-KY-THUAT.md` Mục 8 và Mục 17, điểm #2). Nếu Mediworld muốn đổi sang hướng khác (không mẫu cố định, tự gắn thẻ người duyệt), sửa `proposal-types.ts` một mình sẽ không đủ — cần đổi cả luồng duyệt, không chỉ đổi danh mục loại phiếu. Nên xác nhận sớm với Mediworld trước khi có nhiều phiếu thật phát sinh, để nếu cần đổi thì đổi lúc còn ít dữ liệu.

## 4. Sao lưu và khôi phục

Dữ liệu cần sao lưu gồm **hai phần luôn đi cùng nhau**: cơ sở dữ liệu và thư mục tệp tải lên (`crm-uploads` / `UPLOAD_DIR`). Thiếu một trong hai thì khôi phục không đầy đủ (ví dụ đơn còn nhưng ảnh chuyển khoản mất).

**Sao lưu tay** (chạy trong SSH, thay thông tin kết nối theo `.env.local`):

```bash
pg_dump -U <user> -h localhost -Fc <tên_database> -f ~/sao-luu/crm-$(date +%F).dump
tar czf ~/sao-luu/crm-uploads-$(date +%F).tar.gz -C ~ crm-uploads
```

**Lịch tự động:** thêm vào Cron Jobs của cPanel, chạy 02:00 hằng đêm (sau job phân hạng lúc 01:00), giữ 14 bản gần nhất:

```bash
0 2 * * * pg_dump -U <user> -h localhost -Fc <tên_database> -f ~/sao-luu/crm-$(date +\%F).dump && find ~/sao-luu -name 'crm-*' -mtime +14 -delete
```

**Khôi phục:**

```bash
pg_restore -U <user> -h localhost -d <tên_database> --clean --if-exists ~/sao-luu/crm-2026-09-27.dump
tar xzf ~/sao-luu/crm-uploads-2026-09-27.tar.gz -C ~
```

Sau khi khôi phục, chạy `npm run db:push` (áp các thay đổi cấu trúc mới hơn bản sao lưu, an toàn khi chạy lại) rồi Restart ứng dụng. **Không bao giờ** chạy `npm run db:seed` trên dữ liệu thật — nó xoá dữ liệu để nạp dữ liệu mẫu.

Nên **thử khôi phục vào một database trống** mỗi quý một lần; bản sao lưu chưa từng thử khôi phục thì chưa chắc dùng được. Nên chép bản sao lưu ra nơi khác hosting (máy công ty, ổ đám mây) — sao lưu của chính hosting có thể mất cùng hosting.

## 5. Giám sát và phát hiện sự cố sớm

- **Điểm kiểm tra sức khoẻ:** `GET /api/suc-khoe` — không cần đăng nhập, không trả dữ liệu nghiệp vụ; trả `{"ok":true}` khi ứng dụng chạy và kết nối được cơ sở dữ liệu, trả mã 503 khi mất kết nối. Đăng ký địa chỉ này vào một dịch vụ giám sát miễn phí (UptimeRobot, Better Stack…), kiểm tra mỗi 5 phút, báo về email/Zalo của người phụ trách.
- **Job hằng đêm:** ghi vào `~/crm-nightly.log`; mỗi lần chạy in số đại lý phân hạng, số cảnh báo và "Hoàn tất sau …s". Kiểm tra dòng cuối của tệp mỗi tuần; nếu ngày của dòng cuối cũ hơn 2 ngày là job đang hỏng (dự báo và cảnh báo sẽ cũ).
- **Nhật ký thao tác:** Danh mục → Nhật ký thao tác (chỉ Ban giám đốc), xem mục 7.
- **Nhật ký ứng dụng:** nơi Passenger/cPanel ghi lỗi của Node (`stderr.log` trong thư mục ứng dụng, tuỳ nhà cung cấp); lỗi nghiệp vụ hiển thị cho người dùng bằng thông báo tiếng Việt, lỗi hệ thống ghi vào đây.
- Dung lượng đĩa: thư mục `crm-uploads` tăng theo số ảnh check-in và tệp đính kèm. Ảnh check-in được nén trước khi lưu; kiểm tra `df -h` mỗi quý.

## 6. Quy trình cập nhật và nâng cấp

1. Sao lưu (mục 4) trước mọi lần cập nhật.
2. Làm theo mục "12. Cập nhật phiên bản về sau" của `HUONG-DAN-TRIEN-KHAI-HAWKHOST.md` (upload → `npm install --include=dev` nếu `package.json`/`package-lock.json` đổi → `npm run db:push` nếu có migration mới → build → Restart). Mỗi bản phát hành đều ghi rõ có migration hay không trong cùng tài liệu đó.
3. Sau khi Restart: mở `/api/suc-khoe`, đăng nhập thử bằng một tài khoản Ban giám đốc và một tài khoản nhân viên, mở vài trang chính.

**Thêm tính năng về sau:** cấu trúc thư mục theo từng phân hệ (`src/app/(app)/<phân hệ>`), truy vấn dùng chung ở `src/lib`, mọi truy vấn đều lọc theo công ty và phạm vi người dùng qua các hàm ở `src/lib/rbac.ts`. Quy tắc bắt buộc khi viết mã mới được liệt kê ở đầu `CLAUDE.md` (không tin mã do trình duyệt gửi lên, mọi thao tác ghi phải ghi nhật ký, thao tác server trả `{ ok | error }`…). Các thay đổi cấu trúc dữ liệu đi qua migration (`npm run db:generate`), không sửa tay cơ sở dữ liệu.

**Kiểm tra trước khi phát hành:** `npm run lint`, `npm run typecheck`, `npm run build` đều phải sạch. Hiện dự án **chưa có bộ kiểm thử tự động**; việc kiểm tra được làm bằng bộ test case thủ công và kịch bản đối chiếu SQL (xem `trao-doi-khach/bo-test-case-2026-09-19.xlsx` nếu được bàn giao kèm). Nếu bảo trì lâu dài, nên bổ sung kiểm thử tự động cho các quy tắc quan trọng: phân quyền theo vai trò, khoá thông tin đã duyệt, tính doanh thu/công nợ.

**Theo dõi lỗ hổng thư viện:** chạy `npm audit --omit=dev` mỗi tháng. Tình trạng ngày 27/09/2026: đã vá `next`, `nodemailer`, `sharp`; còn `drizzle-orm` (cần nâng lên bản 0.45 — nâng bản chính, chưa ảnh hưởng vì hệ thống không đưa tên cột do người dùng nhập vào SQL), `uuid` qua `exceljs` (chỉ ảnh hưởng khi truyền bộ đệm cho `uuid`, hệ thống không làm vậy) và `postcss` nằm bên trong `next` (chỉ dùng lúc build). Kế hoạch: nâng `drizzle-orm` và `next` lên bản chính mới trong một nhánh riêng có thời gian kiểm thử.

## 7. Nhật ký chỉnh sửa và khoá thông tin đã duyệt

**Nhật ký (yêu cầu bổ sung của khách):**

- Mọi thao tác tạo, sửa, xoá dữ liệu nghiệp vụ và cấu hình đều được ghi: ai, lúc nào, làm gì, và với thao tác sửa là từng trường "cũ → mới" (kể cả dòng hàng của đơn). Thao tác xoá lưu lại nội dung bản ghi bị xoá vì sau đó không còn gì để đối chiếu. **Mật khẩu không bao giờ được ghi**, kể cả dạng đã băm.
- Chỉ **Ban giám đốc** xem được: Danh mục → Nhật ký thao tác (tìm kiếm, lọc theo người/hành động/đối tượng/ngày, phân trang, xuất Excel) và khung "Lịch sử chỉnh sửa" ở đơn hàng, hồ sơ khách hàng, hồ sơ nhân viên và từng phiếu đề xuất.
- Ứng dụng không có thao tác nào sửa hoặc xoá dòng nhật ký. Người có quyền quản trị trực tiếp cơ sở dữ liệu vẫn sửa được về mặt kỹ thuật; nếu cần đảm bảo chặt hơn, nên đặt quyền ghi vào bảng `audit_logs` chỉ cho tài khoản ứng dụng (chỉ thêm, không sửa/xoá) hoặc chép nhật ký định kỳ ra nơi khác.
- Các thao tác thường ngày mang tính cá nhân, không thay đổi dữ liệu đã có (check-in, tick việc trong lộ trình, đánh dấu đã đọc thông báo) không được ghi để tránh làm loãng nhật ký.

**Thông tin cần duyệt không được sửa hay xoá, trừ Admin:**

| Đối tượng | Người lập | Quản lý, Giám đốc khu vực | Ban giám đốc (Admin) |
|---|---|---|---|
| Đơn hàng chờ duyệt | sửa được đến khi duyệt | duyệt hoặc trả về điều chỉnh (không sửa hộ) | sửa, duyệt, huỷ, xoá |
| Đơn hàng đã duyệt | **không sửa, không huỷ, không xoá, không gỡ tệp** | **không sửa, không huỷ, không xoá, không gỡ tệp** | sửa, huỷ, xoá, gỡ tệp (có nhật ký "cũ → mới") |
| Phiếu đề xuất | thu hồi được khi chưa ai duyệt; không sửa/xoá | duyệt hoặc từ chối đúng lượt; không sửa/xoá | sửa nội dung ở mọi trạng thái, xoá (chuỗi duyệt và kết quả duyệt giữ nguyên khi sửa) |

Hai điểm có chủ đích cần khách xác nhận: (1) quản lý vẫn xoá được phiếu thu ghi nhầm của đơn đã duyệt (có nhật ký); (2) mọi người có quyền xem đơn vẫn đính kèm thêm được tệp vào đơn đã duyệt (ví dụ ảnh chuyển khoản) nhưng không gỡ được.

## 8. Bảo mật — hiện trạng đã kiểm chứng

- Mật khẩu băm scrypt; phiên đăng nhập là cookie `httpOnly`, `SameSite=Lax`, thêm `Secure` khi chạy thật; khoá tài khoản 15 phút sau nhiều lần nhập sai; khoá tài khoản có hiệu lực tức thì ở request kế tiếp.
- Mọi truy vấn lọc theo công ty (multi-tenant) và theo phạm vi vai trò; ảnh và tệp chỉ mở được khi người xem có quyền xem đối tượng chứa chúng.
- Header bảo mật trên mọi phản hồi: chặn nhúng khung (`X-Frame-Options`, `frame-ancestors`), `nosniff`, `Referrer-Policy`, `Permissions-Policy`, `Strict-Transport-Security` (khi chạy thật); không lộ phiên bản framework.
- Tệp tải lên kiểm tra nội dung thật của tệp (không tin loại do trình duyệt khai báo), giới hạn dung lượng; Excel/Word/PDF không được thực thi.
- **Việc phải làm khi đưa vào chạy thật:** đổi `SESSION_SECRET`, đổi toàn bộ mật khẩu mẫu, bật HTTPS, không bao giờ đưa `.env.local` hoặc tệp sao lưu vào kho Git, dùng mật khẩu riêng và mạnh cho mọi tài khoản Ban giám đốc.
- Chưa có giới hạn số lần đăng nhập theo địa chỉ IP (chỉ khoá theo tài khoản). Nếu hosting hỗ trợ, nên bật giới hạn ở tầng máy chủ web/Cloudflare.

## 9. Việc kiểm tra nhanh khi có sự cố

| Triệu chứng | Kiểm tra đầu tiên | Xử lý |
|---|---|---|
| Không vào được web | `/api/suc-khoe` | 503: kiểm tra `DATABASE_URL` trong `.env.local`, PostgreSQL còn chạy; không phản hồi: Restart app trong cPanel |
| Báo "too many connections" | số kết nối | đặt `PG_POOL_MAX=4` trong `.env.local`, Restart |
| Cảnh báo, dự báo, phân hạng cũ | đuôi `~/crm-nightly.log` | chạy tay `npm run jobs:nightly`; kiểm tra cron |
| Người dùng bị khoá đăng nhập | — | `npm run user:unlock` (hoặc chờ 15 phút) |
| Số liệu lệch sau khi nhập bù đơn cũ | — | Danh mục → Chỉ tiêu → "Tính lại số liệu" |
| Lỗi lạ sau cập nhật | `npm run build` có lỗi? | xem mục "Xử lý lỗi" trong `HUONG-DAN-TRIEN-KHAI-HAWKHOST.md` |
| Cần biết ai đã sửa/xoá một thứ gì | Nhật ký thao tác | tìm theo tên, mã đơn, số điện thoại, khoảng ngày |

Tạo lại danh sách giấy phép thư viện: chạy đoạn script sinh `THIRD-PARTY-LICENSES.md` (đọc `package-lock.json`, bỏ các gói chỉ dùng khi phát triển) hoặc `npx license-checker --production --summary`.

## 10. Cam kết bảo trì 2 năm — gợi ý phạm vi để ghi vào phụ lục hợp đồng

Ghi rõ trong hợp đồng thay vì để mơ hồ: (1) **sửa lỗi** — lỗi làm sai số liệu hoặc không dùng được chức năng đã bàn giao, thời gian phản hồi và thời gian khắc phục theo mức độ; (2) **điều chỉnh** — thay đổi nhỏ trong phạm vi chức năng hiện có (nhãn, ngưỡng, báo cáo); (3) **nâng cấp bảo mật** — cập nhật thư viện có lỗ hổng theo mục 6; (4) **tính năng mới** — báo giá riêng. Nên quy định kênh liên hệ hỗ trợ, khung giờ và đầu mối của hai bên. Đây là gợi ý để hai bên thoả thuận, không phải điều khoản pháp lý.
