Skip to content

Kiến trúc kỹ thuật ​

Tổng thể ​

compass.conan1.com          api.conan1.com              D1
Cloudflare Pages            Cloudflare Worker           conan-compass
─────────────────           ──────────────────          ─────────────
Vite 8 + React 19           Hono                        capabilities
Tailwind v4                 SQL thuần                   brands
shadcn + radix-ui           Access JWT verify           brand_capabilities
Heroicons                   Workers AI (binding AI)     processes / activities
Motion                        → AI Gateway `conan1`     applications / application_urls
React Router 7                                          data_entities
                                                        technologies
Ba mặt phẳng:                                           value_streams
  /            Compass                                  initiatives / kpis
  /products    Products                                 products / features / releases
  /requirements Requirements                            requirements / questions
                                                        ai_runs / ai_suggestions
                                                        decisions / decision_links
                                                        evidence
                                                        users / audit_log / sequences

Một SPA phục vụ cả ba mặt phẳng. Mặt phẳng đang mở được suy ra từ đường dẫn (src/lib/plane.ts → planeOf(pathname)), không có state riêng; PlaneSwitch trên thanh tiêu đề chỉ là ba NavLink, và sidebar render nhóm điều hướng của mặt phẳng đang mở.

Vì sao tải toàn bộ mô hình một lần ​

Mô hình chỉ có vài chục năng lực và một nắm Brand — tổng cộng vài chục KB JSON. Khi khởi động, CompassProvider tải:

  1. /v1/me
  2. /v1/capabilities
  3. /v1/brands
  4. /v1/architecture — toàn bộ tầng thực thi, chuyển đổi và hai mặt phẳng Products, Requirements trong một request (sản phẩm, URL, feature, yêu cầu, câu hỏi, quyết định, dải liên quan, bằng chứng, release)
  5. /v1/brands/:code/capabilities cho từng Brand
  6. /v1/ai/status — AI có được cấu hình không (lỗi → coi như không)

Hai bảng không nằm trong gói: ai_suggestions và ai_runs. Trang chi tiết yêu cầu tự tải chúng qua /v1/requirements/:id/suggestions (hook useSuggestions), để reload() không phình theo lượng AI.

Sau đó mọi màn hình phân tích được tính ngay tại client. Cross-Brand Comparison, Heatmap, Gaps, Risk Map và các thẻ trên Overview không cần thêm một round-trip nào, và không cần một endpoint riêng cho từng biểu đồ.

Sau mỗi lần ghi, ứng dụng gọi reload() để tải lại — đơn giản và luôn nhất quán với server. Nếu sau này mô hình phình lên hàng nghìn năng lực, đây là chỗ đầu tiên cần đổi.

Cấu trúc mã nguồn ​

compass.conan1.com/
  src/
    lib/
      api.ts        Client gọi api.conan1.com
      store.tsx     CompassProvider — nguồn dữ liệu duy nhất
      scales.ts     Thang điểm, công thức risk và gap
      plane.ts      planeOf(pathname), planeHome
      trace.ts      traceOf, requirementsOf, decisionsFor, productsOfCapability
      fields.ts     bilingualFields, brandField, applicationFields, enumField
      suggestions.ts useSuggestions(requirementId) — tự tải gợi ý AI
      glossary.ts   Thuật ngữ song ngữ, planes
      types.ts
    i18n/
      dictionary.ts Từ điển Việt – Anh
      index.tsx     I18nProvider, useI18n
    components/
      ui/           shadcn
      AppShell.tsx  Sidebar theo mặt phẳng, topbar, chuyển trang
      PlaneSwitch.tsx  Compass / Products / Requirements
      Score.tsx     ScoreChip, RiskChip, GapChip
      TraceChain.tsx   Chuỗi chip Capability → … → Requirement
      QuestionPanel.tsx · SuggestionPicker.tsx · DecideDialog.tsx
      *Dialog.tsx   Form thêm/sửa
    pages/          Mỗi route một file
      Products.tsx · ProductDetail.tsx · ProductApplications.tsx · ApplicationDetail.tsx
      Requirements.tsx · RequirementDetail.tsx · Questions.tsx · Decisions.tsx · DecisionDetail.tsx

Nguồn dữ liệu duy nhất ​

useCompass() trả về mô hình đã chuẩn hoá cùng vài chỉ mục dựng sẵn:

TrườngÝ nghĩa
byIdMap<capability_id, Capability>
childrenOfMap<parent_id | null, Capability[]> đã sắp xếp
pathOf(id)Đường dẫn từ L1 tới năng lực đó, dùng cho breadcrumb
maps[brandCode][capabilityId]Đánh giá của một Brand
archTầng thực thi: process, application, data, technology, value stream, initiative, KPI, objective, key result — cộng product, productCapabilities, applicationUrls, feature, requirement, question, decision, decisionLinks, evidence, release, releaseRequirements
ai{ enabled, model, gateway } từ /v1/ai/status; các nút Gợi ý mờ khi enabled = false
canEditModel, canEditBrand(code), canEditScopeQuyền, tính từ vai trò, phạm vi đang chọn và chế độ Xem/Sửa
editMode, setEditMode, mayEditChế độ hiện tại, và liệu người dùng có quyền sửa gì không

Các hook phụ trợ trả về dữ liệu đã lọc theo bộ chọn phạm vi, nên mỗi trang chỉ cần lo phần trình bày:

HookTrả về
useScopedBrands()Brand trong phạm vi
useScopedRows()Mọi đánh giá năng lực trong phạm vi, đã làm phẳng
useScopeBrandId()brand_id hiện tại, hoặc null ở Enterprise
inScope(rows, brandId)Lọc bản ghi của Brand cộng bản ghi dùng chung
indexLinks(links, from, to)Gom bảng nối thành Map<a, b[]>

Animation ​

AppShell bọc nội dung trong AnimatePresence mode="wait" với key là location.pathname: fade + dịch 6px, 180ms. Danh sách và thẻ fade-in so le, độ trễ tối đa 0.24s. prefers-reduced-motion được tôn trọng ở tầng CSS.

Toàn bộ cấu hình animation nằm trong src/lib/motion.ts (enter() và pageTransition()), không rải rác trong từng trang.

Tab ẩn

Animation chạy trên requestAnimationFrame, mà trình duyệt bóp nghẹt rAF ở tab đang ẩn. Một phần tử bắt đầu ở opacity: 0 sẽ đứng yên vô hình cho tới khi người dùng chuyển sang tab đó — nghĩa là trang mở ở tab nền hiện ra trắng trơn.

Vì vậy enter() và pageTransition() kiểm tra document.visibilityState khi mount: nếu tài liệu đang ẩn thì bỏ animation và render thẳng trạng thái cuối.