A1 · Backend perf
SLA cron batch label qua 1 query whereIn (thay vì pluck per-conversation) +
select hẹp cột + cảnh báo tick >30s. automationInFlight guard chuyển
vào services/automation/engine.ts để sla.ts dùng chung runGuarded.
Mọi agent có helpdesk:read thấy mọi conversation, không phân biệt inbox/team/assignee — chủ ý cho team nhỏ, không phải thiếu sót. Đổi lại: mọi list/counts endpoint từ Wave 2 (B1) trở đi bắt buộc nhận filter param (inbox_id, assignee_id, …) dù server chưa dùng chúng như authorization boundary — để khi cần row-scoping thật, chỉ thêm 1 lớp WHERE server-side, không phải đổi API shape/client contract.
A1 · Backend perf
SLA cron batch label qua 1 query whereIn (thay vì pluck per-conversation) +
select hẹp cột + cảnh báo tick >30s. automationInFlight guard chuyển
vào services/automation/engine.ts để sla.ts dùng chung runGuarded.
A2 · FE cleanup
Composer surface lỗi gửi (toast), xoá dead code, :popper v3→v4 (8 chỗ),
bỏ mock folder khỏi production path, xoá 5 plugin no-op.
B1 · Pagination + counts
List conversations server-side filter/sort/page; endpoint GET /conversations/counts (Knex aggregate 1 lần); FE fetchPage +
ensureOne (fix deep-link) + ticker merge-not-replace.
B2 · Reports aggregation
GET /helpdesk/reports/overview Knex aggregate (COUNT, AVG response/
resolution, CSAT) — không load message content; xoá fan-out
ensureLoadedForMany phía FE.
| Endpoint | Default | Clamp max | Ghi chú |
|---|---|---|---|
List CRUD chung (crud.factory.ts) |
100 | 500 | Kể cả limit=-1 (sentinel “không giới hạn” kiểu cũ) cũng bị clamp về 500 |
/messages (flat) |
50 | 200 | |
/conversations/:id/messages (thread lồng) |
không giới hạn mặc định | 1000 | Thread dài hiếm khi vượt — chủ ý không cắt mặc định như list |
conversations list (B1) |
25 (PAGE_SIZE FE) |
100 (server cap) | refetchCurrent gộp page 1..currentPage trong 1 request, cap ở 100 |
IMAP MAX_PER_TICK |
— | 50 message/connection/tick | Cron * * * * * → throughput cap thực tế 50 msg/phút/connection |
| Messages store client (eviction) | — | 2000 message / 20 conversation | LRU theo conversation, không theo từng message lẻ |
Vượt bất kỳ cap nào ở trên không mất dữ liệu — chỉ đợi request/tick kế tiếp (list phân trang tiếp, IMAP watermark chỉ tiến tới message cuối xử lý xong).
Cả 3 đều là state sống trong RAM của một process Node, không chia sẻ qua Redis/DB. Chạy 1 instance thì an toàn tuyệt đối; chạy ≥2 instance thì có khoảng hở nhất định.
app-access.ts)const cache = new Map<string, CachedPermissions>();const CACHE_TTL_MS = 60_000;Key = policy id đã sort (không phải theo user). Cache không invalidate theo event — chỉ hết hạn theo TTL. Thu hồi quyền trên 1 instance vẫn có thể được instance khác “chấp nhận” tới 60s sau nếu request rơi vào đúng instance đó.
runGuarded (chuyển vào services/automation/engine.ts từ đợt A1) chỉ chặn re-entrant automation trong cùng process — 2 instance nhận cùng lúc 1 event status_changed từ 2 request khác nhau vẫn có thể chạy automation song song ở 2 process riêng biệt.
pollInFlight guard// In-process guard so a slow tick (mailbox chậm) doesn't overlap the next tick// within the SAME instance. Cross-instance is handled by context.schedule.let pollInFlight = false;Đây là boolean module-level, chỉ ngăn tick chồng tick trong 1 instance. An toàn cross-instance (không 2 instance cùng poll 1 connection) được uỷ quyền hoàn toàn cho context.schedule của ODP core (leader-election kiểu SynchronizedClock) — nếu deploy custom scheduler không tương thích cơ chế này, có nguy cơ 2 instance cùng poll 1 mailbox.
isEncrypted() là heuristic prefix-string (crypto.ts), không phải structural check — đủ dùng cho secret nội bộ nhưng không phải bằng chứng mã hoá tuyệt đối; đừng tái dùng để validate input từ nguồn không tin cậy.i18n/locales/en.json/vi.json thiếu nhiều key, phần lớn UI vẫn hardcode tiếng Anh.navigations.ts), nhưng UI chưa tự ẩn/disable từng nút theo capability; server vẫn chặn đúng qua 403, chỉ là UX chưa mượt (user bấm rồi mới thấy lỗi).ensureOne