1 · UI (Vue/Nuxt)
Pages + components + composables trong Nuxt layer. Không gọi thẳng backend — mọi request đi qua BFF cùng origin.
┌─────────────────────────────────────────────────────────────┐│ Admin console (Nuxt 4, NuxtUI v4) ││ admin/packages/helpdesk ← Nuxt layer ││ app/ pages · components · composables (14 entity stores) ││ ▲ ││ │ $fetch (cookie-auth) ││ server/api/helpdesk/** ← BFF: assertPermission + proxy │└─────────┼─────────────────────────────────────────────────────┘ │ @odp/sdk (customEndpoint, userApi.request) ▼┌─────────────────────────────────────────────────────────────┐│ ODP API server (@odp/api, Fastify + Knex) ││ backend/extensions/helpdesk ← bundle extension ││ endpoints/ routes · controllers · services ││ hooks/ filter/action lifecycle + cron schedules ││ ▲ ││ │ ItemsService / FilesService / getSchema / database ││ ▼ ││ ODP core: permissions · activity · events · Knex · storage │└─────────────────────────────────────────────────────────────┘ │ ▼ Postgres/MySQL/SQLite (hd_* collections + c_contacts + odp_*)1 · UI (Vue/Nuxt)
Pages + components + composables trong Nuxt layer. Không gọi thẳng backend — mọi request đi qua BFF cùng origin.
2 · BFF
server/api/helpdesk/** proxy sang /helpdesk/* của backend, đính token từ
cookie, chặn quyền bằng assertPermission.
3 · Extension
Endpoints (HTTP) + hooks (lifecycle) + schedules (cron). Đây là nơi chứa toàn bộ logic nghiệp vụ server-side.
4 · ODP core
ItemsService (permission + activity + events), Knex, FilesService, cron đa-instance-safe, storage driver.
Đọc danh sách hội thoại (agent mở màn hình):
UI store useHelpdeskConversations.fetchPage(query) → $fetch GET /api/helpdesk/conversations?page&limit&status&… (BFF) → assertPermission({module:'helpdesk', action:'read'}) → userApi.request(customEndpoint(GET /helpdesk/conversations)) (SDK) → withAccess(ctx,'helpdesk','read', ConversationsController.list) → ItemsService.readByQuery + MetaService (total_count/filter_count) → hydrateConversations (label_ids + requester từ c_contacts) → normalizeConversation (mask + stringify id)Email vào thành ticket (nền, không cần UI):
cron mỗi phút → pollImapConnections → mỗi hd_connections (inbound_provider=imap) → ImapFlow fetch mail mới → simpleParser → processInboundEmail (pipeline 8 bước) → resolve inbox → loop-guard → sender filter → sanitize → dedup theo Message-ID → ensureContactForRequester → threading → tạo/append hd_messages → hook fire automation + SLAMaster/Satellite cho danh tính
Tên/email/phone chỉ sống trên c_contacts (master, chia sẻ với module
Contacts/HR). Helpdesk chỉ giữ contact_id. Mọi lần tạo/tìm contact đi qua
contact-choke-point duy nhất. → Chi tiết
Server sở hữu automation & SLA
Automation, SLA recompute, snooze auto-reopen chạy bằng hooks + cron server-side — không phụ thuộc tab trình duyệt nào đang mở. Client chỉ re-read. → SLA & Snooze
ItemsService, không raw SQL
CRUD collection đi qua ItemsService để có permission + activity + event.
Raw Knex chỉ cho aggregation, junction, denormalized counter, watermark —
luôn có comment giải thích.
Trust model module-level (v1)
createItemsService ép admin:true; authorization dồn về guard
withAccess(module, action) ở tầng route. Chưa có row-scoping theo
inbox/team — chủ ý cho team nhỏ. → Trust model
dist → restart server (không có auto-reload). → Cấu trúc & vòng đời:8055. Thêm route backend thì phải thêm route BFF tương ứng.directus_* → odp_*. Bảng hệ thống là odp_users, odp_files, odp_roles… không còn tiền tố directus_.hd_*