Giao diện
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 / sequencesMộ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:
/v1/me/v1/capabilities/v1/brands/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)/v1/brands/:code/capabilitiescho từng Brand/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.tsxNguồ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 |
|---|---|
byId | Map<capability_id, Capability> |
childrenOf | Map<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 |
arch | Tầ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), canEditScope | Quyền, tính từ vai trò, phạm vi đang chọn và chế độ Xem/Sửa |
editMode, setEditMode, mayEdit | Chế độ 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:
| Hook | Trả 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.