@anlyx/ui 0.1.3 → 0.1.6-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/capture/capture-runtime.d.ts +14 -0
- package/dist/capture/capture-runtime.js +300 -0
- package/dist/components/CaptureStatusEmptyState.js +2 -2
- package/dist/components/EndpointMapCanvas.js +1 -1
- package/dist/components/InspectorPanel.js +27 -1
- package/dist/components/StatusBadge.d.ts +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/overlay/MainFlowCanvas.js +1 -1
- package/dist/overlay/overlay-ui.js +1 -1
- package/dist/readme-demo/ReadmeDemoApp.d.ts +12 -1
- package/dist/readme-demo/ReadmeDemoApp.js +74 -16
- package/dist/styles.css +17 -4
- package/dist/viewer/ViewerApp.js +35 -15
- package/dist/viewer/styles.css +2639 -0
- package/dist/viewer/viewer-entry.d.ts +1 -0
- package/dist/viewer/viewer-entry.js +1 -0
- package/dist/viewer/workspace/anlyx-logo-transparent.png +0 -0
- package/dist/viewer/workspace/workspace.css +11938 -0
- package/dist/workspace/ScanTreeMap.d.ts +6 -0
- package/dist/workspace/ScanTreeMap.js +838 -0
- package/dist/workspace/WorkspaceApp.d.ts +9 -0
- package/dist/workspace/WorkspaceApp.js +3016 -0
- package/dist/workspace/project-view-model.d.ts +63 -0
- package/dist/workspace/project-view-model.js +170 -0
- package/dist/workspace/workspace.css +11938 -0
- package/package.json +10 -2
|
@@ -0,0 +1,3016 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { BaseEdge, Handle, Position, ReactFlow, getSmoothStepPath } from "@xyflow/react";
|
|
3
|
+
import { BookOpen, Box, Braces, BriefcaseBusiness, Check, ChevronRight, Circle, Clock3, Code2, Copy, Database, Download, FileCode2, FileText, Flag, Folder, Gauge, Layers3, Languages, Lock, Minus, MousePointerClick, Network, PanelLeft, Plus, RotateCw, Search, ShieldCheck, Tag, Workflow, Zap } from "lucide-react";
|
|
4
|
+
import { siExpress, siNodedotjs, siReact, siTypescript } from "simple-icons";
|
|
5
|
+
import { Fragment, createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { ScanTreeMap } from "./ScanTreeMap.js";
|
|
7
|
+
import { buildProjectWorkspaceViewModel } from "./project-view-model.js";
|
|
8
|
+
const WorkspaceLocaleContext = createContext("en");
|
|
9
|
+
const TIMELINE_START_INSET_PERCENT = 2.5;
|
|
10
|
+
const translations = {
|
|
11
|
+
en: {
|
|
12
|
+
appLabel: "Anlyx live workspace",
|
|
13
|
+
sidebarLabel: "Workspace navigation",
|
|
14
|
+
primaryNavLabel: "Primary",
|
|
15
|
+
flows: "Flows",
|
|
16
|
+
docs: "Docs",
|
|
17
|
+
language: "Language",
|
|
18
|
+
flowViews: "Flow views",
|
|
19
|
+
flow: "Flow",
|
|
20
|
+
waiting: "waiting",
|
|
21
|
+
lastScan: "Last import",
|
|
22
|
+
matchedBackendFlow: "Matched backend flow",
|
|
23
|
+
liveWorkspace: "Live workspace",
|
|
24
|
+
captureConnected: "Capture connected",
|
|
25
|
+
waitingForCapture: "Waiting for capture",
|
|
26
|
+
matchedSubtitle: "Browser request first, source-matched backend path follows.",
|
|
27
|
+
readyPrefix: "is ready. Use your local app and Anlyx will stream requests here.",
|
|
28
|
+
readySuffix: "",
|
|
29
|
+
selectedRequest: "Selected request",
|
|
30
|
+
selectedRequestLabel: "Selected request",
|
|
31
|
+
currentPage: "Current page",
|
|
32
|
+
pageUrlNotCaptured: "Page URL not captured",
|
|
33
|
+
requestEvidenceSummary: "Request evidence summary",
|
|
34
|
+
mappedEvidence: "Browser-observed request mapped to imported source evidence.",
|
|
35
|
+
mappedEvidenceWithRuntime: "Browser-observed request mapped to imported source evidence and development runtime spans.",
|
|
36
|
+
controllerPending: "Controller pending",
|
|
37
|
+
flowTiming: "Flow timing",
|
|
38
|
+
totalDuration: "Total duration",
|
|
39
|
+
fit: "Fit",
|
|
40
|
+
focusSlowestSegment: "Focus slowest segment",
|
|
41
|
+
zoomIn: "Zoom in",
|
|
42
|
+
totalDurationOverview: "Total duration overview",
|
|
43
|
+
slowestShownSegment: "Slowest shown segment",
|
|
44
|
+
startsAt: "starts at",
|
|
45
|
+
layer: "Layer",
|
|
46
|
+
node: "Node",
|
|
47
|
+
duration: "Duration",
|
|
48
|
+
notProven: "not proven",
|
|
49
|
+
knownBySourceOnly: "known by source evidence only",
|
|
50
|
+
bottleneck: "Bottleneck",
|
|
51
|
+
slowestSourceSegment: "Slowest source segment",
|
|
52
|
+
blocked: "blocked",
|
|
53
|
+
flowSummary: "Flow summary",
|
|
54
|
+
observedSourceMatchedPath: "Observed / source-matched path",
|
|
55
|
+
summaryMatchedCopy: "Browser-observed request first, then imported source evidence.",
|
|
56
|
+
layers: "layers",
|
|
57
|
+
knownDownstreamPath: "Known downstream path",
|
|
58
|
+
summaryNote: "Anlyx maps this browser-observed request to imported source evidence. Source-matched rows are not a production runtime trace; muted rows were not proven executed.",
|
|
59
|
+
flowDiagram: "Flow diagram",
|
|
60
|
+
observedSourceMatchedLegend: "Observed / source-matched path",
|
|
61
|
+
knownDownstreamLegend: "Known downstream (not proven)",
|
|
62
|
+
entryPoint: "Entry point",
|
|
63
|
+
fitView: "Fit view",
|
|
64
|
+
reset: "Reset",
|
|
65
|
+
zoomOut: "Zoom out",
|
|
66
|
+
authDecision: "Auth / Decision",
|
|
67
|
+
backend: "Backend",
|
|
68
|
+
service: "Service",
|
|
69
|
+
repository: "Repository",
|
|
70
|
+
database: "Database",
|
|
71
|
+
untrackedApp: "Untracked app",
|
|
72
|
+
untrackedRuntime: "not yet split into lower layers",
|
|
73
|
+
noScannedNode: "No source node matched",
|
|
74
|
+
downstreamMatched: "Downstream path matched by browser request and source evidence",
|
|
75
|
+
knownScannedNotProven: "Known source path, not proven executed",
|
|
76
|
+
inferred: "Inferred",
|
|
77
|
+
matched: "Matched",
|
|
78
|
+
evidenceInspector: "Evidence inspector",
|
|
79
|
+
anlyxFlow: "Anlyx Flow",
|
|
80
|
+
recentRequests: "Recent requests",
|
|
81
|
+
background: "Background",
|
|
82
|
+
currentRequestWindow: "Current page/action only",
|
|
83
|
+
request: "Request",
|
|
84
|
+
method: "Method",
|
|
85
|
+
path: "Path",
|
|
86
|
+
outcome: "Outcome",
|
|
87
|
+
mappingConfidence: "Mapping confidence",
|
|
88
|
+
evidenceUsed: "Evidence used",
|
|
89
|
+
coverage: "Coverage",
|
|
90
|
+
traceNote: "Trace note",
|
|
91
|
+
keepWorkspaceOpen: "Keep this workspace open, then use your local app. Captured requests will appear here.",
|
|
92
|
+
firstRequestHint: "Click your local app to stream the first browser request.",
|
|
93
|
+
waitingForBrowserRequests: "Waiting for browser requests",
|
|
94
|
+
liveCaptureReady: "Live capture ready",
|
|
95
|
+
noRequestSelected: "No request selected yet",
|
|
96
|
+
noCurrentPageRequests: "No requests observed on this page",
|
|
97
|
+
noCurrentPageRequestsCopy: "Anlyx captured the current page, but no API request has been observed for this page/action yet.",
|
|
98
|
+
emptyWorkspaceCopy: "No flow entries are loaded yet. Add Flow JSON to populate this workspace.",
|
|
99
|
+
requestProcessing: "Request/Processing",
|
|
100
|
+
application: "Application",
|
|
101
|
+
authDecisionLegend: "Auth / Decision",
|
|
102
|
+
result: "Result",
|
|
103
|
+
pending: "Pending",
|
|
104
|
+
ok: "OK",
|
|
105
|
+
unauthorized: "Unauthorized",
|
|
106
|
+
forbidden: "Forbidden",
|
|
107
|
+
conflict: "Conflict",
|
|
108
|
+
serverError: "Server error",
|
|
109
|
+
observed: "observed",
|
|
110
|
+
sourceMatched: "source matched",
|
|
111
|
+
browserSpan: "browser span",
|
|
112
|
+
serverRequestSpan: "server request span",
|
|
113
|
+
browserObserved: "browser observed",
|
|
114
|
+
devRuntimeSpan: "dev runtime span",
|
|
115
|
+
measuredRuntime: "measured runtime",
|
|
116
|
+
callCount: "calls",
|
|
117
|
+
sourceDerivedEstimate: "source-derived estimate",
|
|
118
|
+
responseMarker: "response marker",
|
|
119
|
+
estimate: "estimate",
|
|
120
|
+
browser: "Browser",
|
|
121
|
+
source: "Source",
|
|
122
|
+
backendCoverage: "Backend"
|
|
123
|
+
},
|
|
124
|
+
ko: {
|
|
125
|
+
appLabel: "Anlyx 라이브 워크스페이스",
|
|
126
|
+
sidebarLabel: "워크스페이스 내비게이션",
|
|
127
|
+
primaryNavLabel: "주 메뉴",
|
|
128
|
+
flows: "흐름",
|
|
129
|
+
docs: "문서",
|
|
130
|
+
language: "언어",
|
|
131
|
+
flowViews: "흐름 보기",
|
|
132
|
+
flow: "흐름",
|
|
133
|
+
waiting: "대기 중",
|
|
134
|
+
lastScan: "마지막 가져오기",
|
|
135
|
+
matchedBackendFlow: "매칭된 백엔드 흐름",
|
|
136
|
+
liveWorkspace: "라이브 워크스페이스",
|
|
137
|
+
captureConnected: "캡처 연결됨",
|
|
138
|
+
waitingForCapture: "캡처 대기 중",
|
|
139
|
+
matchedSubtitle: "브라우저 요청을 먼저 보고, 소스 매칭 백엔드 경로를 이어서 보여줍니다.",
|
|
140
|
+
readyPrefix: "준비되었습니다. 로컬 앱을 사용하면 Anlyx가 요청을 이곳으로 스트리밍합니다.",
|
|
141
|
+
readySuffix: "",
|
|
142
|
+
selectedRequest: "선택된 요청",
|
|
143
|
+
selectedRequestLabel: "선택된 요청",
|
|
144
|
+
currentPage: "현재 페이지",
|
|
145
|
+
pageUrlNotCaptured: "페이지 URL 미수집",
|
|
146
|
+
requestEvidenceSummary: "요청 근거 요약",
|
|
147
|
+
mappedEvidence: "브라우저에서 관찰된 요청을 가져온 소스 근거와 매칭했습니다.",
|
|
148
|
+
mappedEvidenceWithRuntime: "브라우저에서 관찰된 요청을 가져온 소스 근거와 개발용 런타임 span에 매칭했습니다.",
|
|
149
|
+
controllerPending: "컨트롤러 확인 대기",
|
|
150
|
+
flowTiming: "흐름 타이밍",
|
|
151
|
+
totalDuration: "전체 소요 시간",
|
|
152
|
+
fit: "맞춤",
|
|
153
|
+
focusSlowestSegment: "가장 느린 구간 강조",
|
|
154
|
+
zoomIn: "확대",
|
|
155
|
+
totalDurationOverview: "전체 소요 시간 요약",
|
|
156
|
+
slowestShownSegment: "가장 긴 구간",
|
|
157
|
+
startsAt: "시작",
|
|
158
|
+
layer: "레이어",
|
|
159
|
+
node: "노드",
|
|
160
|
+
duration: "소요 시간",
|
|
161
|
+
notProven: "실행 확인 안 됨",
|
|
162
|
+
knownBySourceOnly: "소스 근거로만 확인됨",
|
|
163
|
+
bottleneck: "병목",
|
|
164
|
+
slowestSourceSegment: "가장 긴 소스 구간",
|
|
165
|
+
blocked: "차단됨",
|
|
166
|
+
flowSummary: "흐름 요약",
|
|
167
|
+
observedSourceMatchedPath: "관찰 / 소스 매칭 경로",
|
|
168
|
+
summaryMatchedCopy: "브라우저 요청을 먼저 보여주고, 이어서 가져온 소스 근거를 보여줍니다.",
|
|
169
|
+
layers: "개 레이어",
|
|
170
|
+
knownDownstreamPath: "알려진 하위 경로",
|
|
171
|
+
summaryNote: "Anlyx는 브라우저에서 관찰된 요청을 가져온 소스 근거와 연결합니다. 소스 매칭 행은 운영 런타임 트레이스가 아니며, 흐리게 표시된 행은 실제 실행이 확인되지 않았습니다.",
|
|
172
|
+
flowDiagram: "흐름 다이어그램",
|
|
173
|
+
observedSourceMatchedLegend: "관찰 / 소스 매칭 경로",
|
|
174
|
+
knownDownstreamLegend: "알려진 하위 경로(실행 미확인)",
|
|
175
|
+
entryPoint: "진입점",
|
|
176
|
+
fitView: "화면 맞춤",
|
|
177
|
+
reset: "초기화",
|
|
178
|
+
zoomOut: "축소",
|
|
179
|
+
authDecision: "인증 / 결정",
|
|
180
|
+
backend: "백엔드",
|
|
181
|
+
service: "서비스",
|
|
182
|
+
repository: "레포지토리",
|
|
183
|
+
database: "데이터베이스",
|
|
184
|
+
untrackedApp: "미분해 앱 시간",
|
|
185
|
+
untrackedRuntime: "하위 레이어로 아직 분해되지 않음",
|
|
186
|
+
noScannedNode: "매칭된 소스 노드 없음",
|
|
187
|
+
downstreamMatched: "브라우저 요청과 소스 근거로 하위 경로가 매칭되었습니다",
|
|
188
|
+
knownScannedNotProven: "소스 근거로 알려진 경로지만 실행은 확인되지 않았습니다",
|
|
189
|
+
inferred: "추론됨",
|
|
190
|
+
matched: "매칭됨",
|
|
191
|
+
evidenceInspector: "근거 인스펙터",
|
|
192
|
+
anlyxFlow: "Anlyx 흐름",
|
|
193
|
+
recentRequests: "최근 요청",
|
|
194
|
+
background: "백그라운드",
|
|
195
|
+
currentRequestWindow: "현재 페이지/액션만",
|
|
196
|
+
request: "요청",
|
|
197
|
+
method: "메서드",
|
|
198
|
+
path: "경로",
|
|
199
|
+
outcome: "결과",
|
|
200
|
+
mappingConfidence: "매핑 신뢰도",
|
|
201
|
+
evidenceUsed: "사용된 근거",
|
|
202
|
+
coverage: "근거 범위",
|
|
203
|
+
traceNote: "트레이스 참고",
|
|
204
|
+
keepWorkspaceOpen: "이 워크스페이스를 열어둔 뒤 로컬 앱을 사용하세요. 캡처된 요청이 이곳에 표시됩니다.",
|
|
205
|
+
firstRequestHint: "로컬 앱을 클릭하면 첫 브라우저 요청이 스트리밍됩니다.",
|
|
206
|
+
waitingForBrowserRequests: "브라우저 요청 대기 중",
|
|
207
|
+
liveCaptureReady: "라이브 캡처 준비됨",
|
|
208
|
+
noRequestSelected: "아직 선택된 요청이 없습니다",
|
|
209
|
+
noCurrentPageRequests: "이 페이지에서 관찰된 요청이 없습니다",
|
|
210
|
+
noCurrentPageRequestsCopy: "현재 페이지는 감지됐지만, 이 페이지/액션에 연결된 API 요청은 아직 관찰되지 않았습니다.",
|
|
211
|
+
emptyWorkspaceCopy: "아직 불러온 flow entry가 없습니다. Flow JSON을 추가하면 이 워크스페이스가 채워집니다.",
|
|
212
|
+
requestProcessing: "요청/처리",
|
|
213
|
+
application: "애플리케이션",
|
|
214
|
+
authDecisionLegend: "인증 / 결정",
|
|
215
|
+
result: "결과",
|
|
216
|
+
pending: "대기 중",
|
|
217
|
+
ok: "정상",
|
|
218
|
+
unauthorized: "인증 필요",
|
|
219
|
+
forbidden: "권한 없음",
|
|
220
|
+
conflict: "충돌",
|
|
221
|
+
serverError: "서버 오류",
|
|
222
|
+
observed: "관찰됨",
|
|
223
|
+
sourceMatched: "소스 매칭",
|
|
224
|
+
browserSpan: "브라우저 구간",
|
|
225
|
+
serverRequestSpan: "서버 요청 구간",
|
|
226
|
+
browserObserved: "브라우저 관찰",
|
|
227
|
+
devRuntimeSpan: "개발 런타임 구간",
|
|
228
|
+
measuredRuntime: "실측 시간",
|
|
229
|
+
callCount: "회 호출",
|
|
230
|
+
sourceDerivedEstimate: "소스 기반 추정",
|
|
231
|
+
responseMarker: "응답 지점",
|
|
232
|
+
estimate: "추정",
|
|
233
|
+
browser: "브라우저",
|
|
234
|
+
source: "소스",
|
|
235
|
+
backendCoverage: "백엔드"
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
function useWorkspaceLocale() {
|
|
239
|
+
return useContext(WorkspaceLocaleContext);
|
|
240
|
+
}
|
|
241
|
+
function t(locale, key) {
|
|
242
|
+
return translations[locale][key] ?? translations.en[key];
|
|
243
|
+
}
|
|
244
|
+
const projectChromeTranslations = {
|
|
245
|
+
en: {
|
|
246
|
+
agent: "AI Agent",
|
|
247
|
+
available: "Available",
|
|
248
|
+
disabled: "Disabled",
|
|
249
|
+
language: "Language",
|
|
250
|
+
lastAnalysis: "Last analysis",
|
|
251
|
+
overview: "Overview",
|
|
252
|
+
capabilities: "Capabilities",
|
|
253
|
+
dataLifecycle: "Data Lifecycle",
|
|
254
|
+
impactMap: "Impact Map",
|
|
255
|
+
map: "Map",
|
|
256
|
+
pages: "Pages",
|
|
257
|
+
source: "Source",
|
|
258
|
+
timing: "Timing",
|
|
259
|
+
upToDate: "Up to date",
|
|
260
|
+
pagesAnalyzed: "pages analyzed"
|
|
261
|
+
},
|
|
262
|
+
ko: {
|
|
263
|
+
agent: "AI Agent",
|
|
264
|
+
available: "사용 가능",
|
|
265
|
+
disabled: "비활성",
|
|
266
|
+
language: "언어",
|
|
267
|
+
lastAnalysis: "마지막 분석",
|
|
268
|
+
overview: "Overview",
|
|
269
|
+
capabilities: "Capabilities",
|
|
270
|
+
dataLifecycle: "Data Lifecycle",
|
|
271
|
+
impactMap: "Impact Map",
|
|
272
|
+
map: "맵",
|
|
273
|
+
pages: "페이지",
|
|
274
|
+
source: "소스",
|
|
275
|
+
timing: "타이밍",
|
|
276
|
+
upToDate: "최신 상태",
|
|
277
|
+
pagesAnalyzed: "개 페이지 분석됨"
|
|
278
|
+
},
|
|
279
|
+
zh: {
|
|
280
|
+
agent: "AI Agent",
|
|
281
|
+
available: "可用",
|
|
282
|
+
disabled: "已停用",
|
|
283
|
+
language: "语言",
|
|
284
|
+
lastAnalysis: "最后分析",
|
|
285
|
+
overview: "Overview",
|
|
286
|
+
capabilities: "Capabilities",
|
|
287
|
+
dataLifecycle: "Data Lifecycle",
|
|
288
|
+
impactMap: "Impact Map",
|
|
289
|
+
map: "地图",
|
|
290
|
+
pages: "页面",
|
|
291
|
+
source: "来源",
|
|
292
|
+
timing: "计时",
|
|
293
|
+
upToDate: "已是最新",
|
|
294
|
+
pagesAnalyzed: "个页面已分析"
|
|
295
|
+
},
|
|
296
|
+
ja: {
|
|
297
|
+
agent: "AI Agent",
|
|
298
|
+
available: "利用可能",
|
|
299
|
+
disabled: "無効",
|
|
300
|
+
language: "言語",
|
|
301
|
+
lastAnalysis: "最終分析",
|
|
302
|
+
overview: "Overview",
|
|
303
|
+
capabilities: "Capabilities",
|
|
304
|
+
dataLifecycle: "Data Lifecycle",
|
|
305
|
+
impactMap: "Impact Map",
|
|
306
|
+
map: "マップ",
|
|
307
|
+
pages: "ページ",
|
|
308
|
+
source: "ソース",
|
|
309
|
+
timing: "タイミング",
|
|
310
|
+
upToDate: "最新",
|
|
311
|
+
pagesAnalyzed: "ページ分析済み"
|
|
312
|
+
},
|
|
313
|
+
fr: {
|
|
314
|
+
agent: "Agent IA",
|
|
315
|
+
available: "Disponible",
|
|
316
|
+
disabled: "Désactivé",
|
|
317
|
+
language: "Langue",
|
|
318
|
+
lastAnalysis: "Dernière analyse",
|
|
319
|
+
overview: "Overview",
|
|
320
|
+
capabilities: "Capabilities",
|
|
321
|
+
dataLifecycle: "Data Lifecycle",
|
|
322
|
+
impactMap: "Impact Map",
|
|
323
|
+
map: "Carte",
|
|
324
|
+
pages: "Pages",
|
|
325
|
+
source: "Source",
|
|
326
|
+
timing: "Timing",
|
|
327
|
+
upToDate: "À jour",
|
|
328
|
+
pagesAnalyzed: "pages analysées"
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
function tp(locale, key) {
|
|
332
|
+
return projectChromeTranslations[locale]?.[key] ?? projectChromeTranslations.en[key];
|
|
333
|
+
}
|
|
334
|
+
function projectDefaultLocale(data) {
|
|
335
|
+
return data.dictionary?.defaultLanguage ?? "en";
|
|
336
|
+
}
|
|
337
|
+
const layerOrder = [
|
|
338
|
+
"action",
|
|
339
|
+
"api",
|
|
340
|
+
"controller",
|
|
341
|
+
"auth",
|
|
342
|
+
"decision",
|
|
343
|
+
"service",
|
|
344
|
+
"repository",
|
|
345
|
+
"database",
|
|
346
|
+
"result"
|
|
347
|
+
];
|
|
348
|
+
const layerIcons = {
|
|
349
|
+
action: Zap,
|
|
350
|
+
api: Network,
|
|
351
|
+
auth: Lock,
|
|
352
|
+
controller: FileCode2,
|
|
353
|
+
database: Database,
|
|
354
|
+
decision: Lock,
|
|
355
|
+
repository: Box,
|
|
356
|
+
result: Flag,
|
|
357
|
+
service: Layers3
|
|
358
|
+
};
|
|
359
|
+
export function WorkspaceApp({ data, projectData, projectValidationReport, streamUrl = "/_anlyx/events/stream", initialRecords = [] }) {
|
|
360
|
+
if (projectData) {
|
|
361
|
+
return (_jsx(ProjectWorkspacePreview, { data: projectData, ...(projectValidationReport ? { validationReport: projectValidationReport } : {}) }));
|
|
362
|
+
}
|
|
363
|
+
if (!data) {
|
|
364
|
+
return (_jsx("main", { className: "project-workspace-preview", role: "application", "aria-label": "Anlyx workspace", children: _jsxs("section", { className: "project-workspace-preview__empty", children: [_jsx("strong", { children: "No Anlyx project data loaded" }), _jsx("span", { children: "Start the local 4777 viewer with anlyx.project.json." })] }) }));
|
|
365
|
+
}
|
|
366
|
+
return _jsx(LegacyWorkspaceApp, { data: data, initialRecords: initialRecords, streamUrl: streamUrl });
|
|
367
|
+
}
|
|
368
|
+
function LegacyWorkspaceApp({ data, streamUrl, initialRecords }) {
|
|
369
|
+
const [records, setRecords] = useState(initialRecords);
|
|
370
|
+
const [selectedId, setSelectedId] = useState(initialRecords[0]?.id);
|
|
371
|
+
const [tab, setTab] = useState("timing");
|
|
372
|
+
const [activeView, setActiveView] = useState("flows");
|
|
373
|
+
const [locale, setLocale] = useState("en");
|
|
374
|
+
const [pageContext, setPageContext] = useState();
|
|
375
|
+
const currentPageRecords = useMemo(() => recordsForPageContext(records, pageContext), [records, pageContext]);
|
|
376
|
+
const currentPagePrimaryRecords = useMemo(() => currentPageRecords.filter(isPrimaryRecord), [currentPageRecords]);
|
|
377
|
+
const selectedRecord = currentPagePrimaryRecords.find((record) => record.id === selectedId) ??
|
|
378
|
+
currentPagePrimaryRecords[0];
|
|
379
|
+
const scopedRecords = scopedRecentRecords(currentPageRecords, selectedRecord);
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
if (currentPagePrimaryRecords.length === 0) {
|
|
382
|
+
if (pageContext && selectedId) {
|
|
383
|
+
setSelectedId(undefined);
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (!selectedId || !currentPagePrimaryRecords.some((record) => record.id === selectedId)) {
|
|
388
|
+
const firstRecord = currentPagePrimaryRecords[0];
|
|
389
|
+
if (firstRecord) {
|
|
390
|
+
setSelectedId(firstRecord.id);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}, [currentPagePrimaryRecords, pageContext, selectedId]);
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (typeof EventSource === "undefined") {
|
|
396
|
+
return undefined;
|
|
397
|
+
}
|
|
398
|
+
const source = new EventSource(streamUrl);
|
|
399
|
+
const handlePageContext = (event) => {
|
|
400
|
+
const context = parsePageContextEvent(event.data);
|
|
401
|
+
if (context) {
|
|
402
|
+
setPageContext(context);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
const handleFlow = (event) => {
|
|
406
|
+
const record = parseFlowRecord(event.data);
|
|
407
|
+
if (!record) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
setRecords((current) => [record, ...current.filter((item) => item.id !== record.id)]);
|
|
411
|
+
setSelectedId((current) => {
|
|
412
|
+
if (isPrimaryRecord(record)) {
|
|
413
|
+
return record.id;
|
|
414
|
+
}
|
|
415
|
+
return current;
|
|
416
|
+
});
|
|
417
|
+
};
|
|
418
|
+
source.addEventListener("page-context", handlePageContext);
|
|
419
|
+
source.addEventListener("flow", handleFlow);
|
|
420
|
+
return () => {
|
|
421
|
+
source.removeEventListener("page-context", handlePageContext);
|
|
422
|
+
source.removeEventListener("flow", handleFlow);
|
|
423
|
+
source.close();
|
|
424
|
+
};
|
|
425
|
+
}, [streamUrl]);
|
|
426
|
+
return (_jsx(WorkspaceLocaleContext.Provider, { value: locale, children: _jsxs("main", { className: `live-workspace live-workspace--${activeView === "flows" ? tab : activeView}`, role: "application", "aria-label": t(locale, "appLabel"), children: [_jsx(WorkspaceSidebar, { activeView: activeView, locale: locale, onLocaleChange: setLocale, onViewChange: setActiveView }), _jsx("section", { className: "workspace-main", children: activeView === "json" ? (_jsx(JsonReaderView, { data: data })) : activeView === "map" ? (_jsx(ScanTreeMap, { data: data })) : (_jsxs("div", { className: "timing-layout", id: "workspace", children: [_jsxs("div", { className: "timing-content", children: [_jsx(WorkspaceHeader, { data: data, record: selectedRecord, recordCount: records.length, activeTab: tab, onTabChange: setTab }), _jsx(RequestContextPanel, { data: data, pageContext: pageContext, record: selectedRecord }), selectedRecord ? (_jsx(WorkspaceContent, { record: selectedRecord, tab: tab })) : (_jsx(EmptyWorkspace, { pageContext: pageContext }))] }), _jsx(FlowInspector, { pageContext: pageContext, record: selectedRecord, records: scopedRecords, selectedId: selectedRecord?.id, onSelect: setSelectedId })] })) })] }) }));
|
|
427
|
+
}
|
|
428
|
+
function ProjectWorkspacePreview({ data, validationReport }) {
|
|
429
|
+
const [activeTab, setActiveTab] = useState("pages");
|
|
430
|
+
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
|
431
|
+
const [projectLocale, setProjectLocale] = useState(() => projectDefaultLocale(data));
|
|
432
|
+
const [selectedPageId, setSelectedPageId] = useState(data.pages[0]?.id);
|
|
433
|
+
const model = useMemo(() => buildProjectWorkspaceViewModel(data, selectedPageId), [data, selectedPageId]);
|
|
434
|
+
const rawJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
setProjectLocale(projectDefaultLocale(data));
|
|
437
|
+
}, [data]);
|
|
438
|
+
return (_jsxs("main", { className: "project-workspace-preview", role: "application", "aria-label": "Anlyx project workspace", children: [_jsx(ProjectTopBar, { model: model, locale: projectLocale, onLocaleChange: setProjectLocale }), _jsxs("div", { className: `project-workspace-shell${isSidebarCollapsed ? " is-sidebar-collapsed" : ""}`, children: [_jsx(ProjectSidebar, { activeTab: activeTab, isCollapsed: isSidebarCollapsed, locale: projectLocale, onTabChange: setActiveTab, onToggleCollapse: () => setIsSidebarCollapsed((value) => !value) }), _jsx("div", { className: "project-workspace-surface", children: activeTab === "pages" ? (_jsx(ProjectPagesWorkspace, { data: data, model: model, ...(validationReport ? { validationReport } : {}), selectedPageId: model.selectedPage?.page.id, onPageSelect: setSelectedPageId, onOpenMap: () => setActiveTab("map") })) : activeTab === "map" ? (_jsx(ProjectMapView, { data: data, model: model })) : activeTab === "overview" ? (_jsx(ProjectOverviewView, { data: data })) : activeTab === "capabilities" ? (_jsx(ProjectCapabilitiesView, { data: data, model: model })) : (_jsx(ProjectJsonView, { data: data, rawJson: rawJson, model: model, ...(validationReport ? { validationReport } : {}) })) })] }), _jsx(ProjectStatusBar, { data: data, locale: projectLocale, model: model, ...(validationReport ? { validationReport } : {}) })] }));
|
|
439
|
+
}
|
|
440
|
+
function readProjectMetadataString(model, key) {
|
|
441
|
+
const value = model.project.metadata?.[key];
|
|
442
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
443
|
+
}
|
|
444
|
+
function projectAgentName(model) {
|
|
445
|
+
return model.project.generatedBy?.name ?? model.project.generatedBy?.kind ?? "AI Agent";
|
|
446
|
+
}
|
|
447
|
+
function projectSourceFile(model) {
|
|
448
|
+
return readProjectMetadataString(model, "sourceFile") ?? "anlyx.project.json";
|
|
449
|
+
}
|
|
450
|
+
function ProjectTopBar({ model }) {
|
|
451
|
+
return (_jsxs("header", { className: "project-topbar", children: [_jsxs("div", { className: "project-topbar__brand", children: [_jsx("img", { alt: "Anlyx", src: "/workspace/anlyx-logo-transparent.png" }), _jsxs("span", { children: ["v", model.schemaVersion] })] }), _jsxs("button", { className: "project-topbar__project", type: "button", children: [_jsx(Folder, { size: 17 }), _jsx("span", { children: model.project.name }), _jsx(ChevronRight, { size: 14 })] }), _jsx("div", { className: "project-topbar__spacer" })] }));
|
|
452
|
+
}
|
|
453
|
+
function projectTabLabel(tab, locale) {
|
|
454
|
+
if (tab === "pages")
|
|
455
|
+
return tp(locale, "pages");
|
|
456
|
+
if (tab === "map")
|
|
457
|
+
return tp(locale, "map");
|
|
458
|
+
if (tab === "overview")
|
|
459
|
+
return tp(locale, "overview");
|
|
460
|
+
if (tab === "capabilities")
|
|
461
|
+
return tp(locale, "capabilities");
|
|
462
|
+
return "JSON";
|
|
463
|
+
}
|
|
464
|
+
function ProjectSidebar({ activeTab, isCollapsed, locale, onTabChange, onToggleCollapse }) {
|
|
465
|
+
const items = [
|
|
466
|
+
{ icon: FileText, tab: "pages" },
|
|
467
|
+
{ icon: Network, tab: "map" },
|
|
468
|
+
{ icon: Gauge, tab: "overview" },
|
|
469
|
+
{ icon: Workflow, tab: "capabilities" },
|
|
470
|
+
{ icon: Braces, tab: "json" }
|
|
471
|
+
];
|
|
472
|
+
return (_jsxs("aside", { className: `project-sidebar${isCollapsed ? " is-collapsed" : ""}`, "aria-label": "Project navigation", children: [_jsxs("div", { className: "project-sidebar__group", children: [isCollapsed ? null : _jsx("h2", { children: "Project" }), items.map(({ icon: Icon, tab }) => (_jsxs("button", { "aria-current": activeTab === tab ? "page" : undefined, "aria-label": isCollapsed ? projectTabLabel(tab, locale) : undefined, className: activeTab === tab ? "is-active" : "", title: isCollapsed ? projectTabLabel(tab, locale) : undefined, type: "button", onClick: () => onTabChange(tab), children: [_jsx(Icon, { size: 17 }), isCollapsed ? null : _jsx("span", { children: projectTabLabel(tab, locale) })] }, tab)))] }), _jsxs("button", { "aria-label": isCollapsed ? "Expand project navigation" : "Collapse project navigation", className: "project-sidebar__collapse", title: isCollapsed ? "Expand" : undefined, type: "button", onClick: onToggleCollapse, children: [_jsx(PanelLeft, { size: 16 }), isCollapsed ? null : _jsx("span", { children: "Collapse" })] })] }));
|
|
473
|
+
}
|
|
474
|
+
function ProjectOverviewView({ data }) {
|
|
475
|
+
const overview = data.overview;
|
|
476
|
+
const stack = projectOverviewStack(overview.implementation);
|
|
477
|
+
const summary = overview.summary ??
|
|
478
|
+
"Inspect the authored Project JSON to understand pages, flows, requests, architecture, evidence, and unknowns.";
|
|
479
|
+
return (_jsx("section", { className: "anlyx-understanding anlyx-overview", "aria-label": "Project overview", children: _jsx("div", { className: "anlyx-overview-layout", children: _jsxs("div", { className: "anlyx-overview-main", children: [_jsxs("header", { className: "anlyx-readme-header", children: [_jsx("h1", { children: "Anlyx overview" }), _jsx("p", { children: summary })] }), _jsxs("section", { className: "anlyx-readme-section", "aria-label": "Built with", children: [_jsx("h2", { children: "Built with" }), stack.length > 0 ? (_jsx("div", { className: "anlyx-stack-grid", children: stack.map((item) => (_jsxs("div", { className: "anlyx-stack-item", children: [_jsx(ProjectStackIconView, { icon: item.icon, tone: item.tone }), _jsxs("div", { children: [_jsx("strong", { children: item.name }), _jsx("small", { children: item.detail })] })] }, item.name))) })) : (_jsx("p", { className: "project-muted", children: "No implementation stack authored." }))] }), _jsxs("section", { className: "anlyx-readme-section", "aria-label": "What it does", children: [_jsx("h2", { children: "What it does" }), _jsxs("div", { className: "anlyx-feature-list", children: [_jsx(ProjectReadmeFeature, { icon: FileText, title: "Inspect pages", description: "Explore the UI surfaces, components, and states that make up your app." }), _jsx(ProjectReadmeFeature, { icon: Workflow, title: "Trace request flows", description: "Follow the path from UI events to API requests and data responses." }), _jsx(ProjectReadmeFeature, { icon: Braces, title: "Review authored JSON", description: "Open the source Project JSON and understand the modeled structure." }), _jsx(ProjectReadmeFeature, { icon: Search, title: "Check evidence & unknowns", description: "See what\u2019s backed by evidence and what needs review or clarification." })] })] })] }) }) }));
|
|
480
|
+
}
|
|
481
|
+
function ProjectStackIconView({ icon, tone }) {
|
|
482
|
+
if (icon.kind === "lucide") {
|
|
483
|
+
const Icon = icon.icon;
|
|
484
|
+
return (_jsx("span", { className: `anlyx-stack-icon is-${tone}`, "aria-hidden": "true", children: _jsx(Icon, { size: 22 }) }));
|
|
485
|
+
}
|
|
486
|
+
return (_jsx("span", { className: `anlyx-stack-icon is-${tone}`, "aria-hidden": "true", style: { "--stack-icon-color": `#${icon.hex}` }, children: _jsx("svg", { role: "img", viewBox: "0 0 24 24", "aria-label": icon.title, children: _jsx("path", { d: icon.path }) }) }));
|
|
487
|
+
}
|
|
488
|
+
function ProjectReadmeFeature({ description, icon: Icon, title }) {
|
|
489
|
+
return (_jsxs("div", { className: "anlyx-readme-feature", children: [_jsx("span", { children: _jsx(Icon, { size: 20 }) }), _jsxs("div", { children: [_jsx("strong", { children: title }), _jsx("p", { children: description })] })] }));
|
|
490
|
+
}
|
|
491
|
+
function projectOverviewStack(implementation) {
|
|
492
|
+
return implementation.slice(0, 5).map((item) => ({
|
|
493
|
+
detail: item.description ?? capitalize(item.kind),
|
|
494
|
+
icon: stackIcon(item.name, item.kind),
|
|
495
|
+
name: stackDisplayName(item.name),
|
|
496
|
+
tone: stackTone(item.name, item.kind)
|
|
497
|
+
}));
|
|
498
|
+
}
|
|
499
|
+
function stackDisplayName(value) {
|
|
500
|
+
if (/react/i.test(value))
|
|
501
|
+
return "React";
|
|
502
|
+
if (/typescript|ts\b/i.test(value))
|
|
503
|
+
return "TypeScript";
|
|
504
|
+
if (/node|runtime|cli/i.test(value))
|
|
505
|
+
return "Node.js";
|
|
506
|
+
if (/express/i.test(value))
|
|
507
|
+
return "Express";
|
|
508
|
+
if (/schema|project json|contract/i.test(value))
|
|
509
|
+
return "Project JSON";
|
|
510
|
+
return value;
|
|
511
|
+
}
|
|
512
|
+
function stackIcon(name, kind) {
|
|
513
|
+
const value = `${name} ${kind}`;
|
|
514
|
+
if (/react/i.test(value))
|
|
515
|
+
return simpleStackIcon(siReact);
|
|
516
|
+
if (/typescript|ts\b/i.test(value))
|
|
517
|
+
return simpleStackIcon(siTypescript);
|
|
518
|
+
if (/node|runtime|cli/i.test(value))
|
|
519
|
+
return simpleStackIcon(siNodedotjs);
|
|
520
|
+
if (/express/i.test(value))
|
|
521
|
+
return simpleStackIcon(siExpress);
|
|
522
|
+
if (/schema|json|contract/i.test(value))
|
|
523
|
+
return { kind: "lucide", icon: Braces };
|
|
524
|
+
return { kind: "lucide", icon: Code2 };
|
|
525
|
+
}
|
|
526
|
+
function simpleStackIcon(icon) {
|
|
527
|
+
return {
|
|
528
|
+
hex: icon.hex,
|
|
529
|
+
kind: "simple",
|
|
530
|
+
path: icon.path,
|
|
531
|
+
title: icon.title
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
function stackTone(name, kind) {
|
|
535
|
+
const value = `${name} ${kind}`;
|
|
536
|
+
if (/react/i.test(value))
|
|
537
|
+
return "react";
|
|
538
|
+
if (/typescript|ts\b/i.test(value))
|
|
539
|
+
return "typescript";
|
|
540
|
+
if (/node|runtime|cli/i.test(value))
|
|
541
|
+
return "node";
|
|
542
|
+
if (/express/i.test(value))
|
|
543
|
+
return "express";
|
|
544
|
+
if (/schema|json|contract/i.test(value))
|
|
545
|
+
return "json";
|
|
546
|
+
return "json";
|
|
547
|
+
}
|
|
548
|
+
function ProjectCapabilitiesView({ data }) {
|
|
549
|
+
const defaultCapability = data.capabilities.find((capability) => /inspect page behavior/i.test(capability.name)) ??
|
|
550
|
+
data.capabilities[0];
|
|
551
|
+
const [selectedId, setSelectedId] = useState(defaultCapability?.id);
|
|
552
|
+
const [capabilityFilter, setCapabilityFilter] = useState("all");
|
|
553
|
+
const visibleCapabilities = data.capabilities.filter((capability) => {
|
|
554
|
+
if (capabilityFilter === "connected")
|
|
555
|
+
return capability.status === "connected";
|
|
556
|
+
if (capabilityFilter === "entry")
|
|
557
|
+
return Boolean(capability.entry);
|
|
558
|
+
if (capabilityFilter === "user")
|
|
559
|
+
return capability.actorRole === "user";
|
|
560
|
+
return true;
|
|
561
|
+
});
|
|
562
|
+
const selected = visibleCapabilities.find((capability) => capability.id === selectedId) ??
|
|
563
|
+
data.capabilities.find((capability) => capability.id === selectedId) ??
|
|
564
|
+
defaultCapability;
|
|
565
|
+
const connected = data.capabilities.filter((capability) => capability.status === "connected").length;
|
|
566
|
+
const userFacing = data.capabilities.filter((capability) => capability.actorRole !== "system").length;
|
|
567
|
+
const unresolved = data.capabilities.filter((capability) => capability.status !== "connected").length;
|
|
568
|
+
return (_jsx("section", { className: "anlyx-understanding anlyx-capabilities", "aria-label": "Project capabilities", children: _jsxs("div", { className: "anlyx-overview-layout", children: [_jsxs("div", { className: "anlyx-overview-main", children: [_jsxs("div", { className: "anlyx-metric-row", children: [_jsx(ProjectUnderstandingMetric, { icon: Workflow, label: "Total capabilities", value: String(data.capabilities.length), detail: "Authored" }), _jsx(ProjectUnderstandingMetric, { icon: Check, label: "Connected", value: String(connected), detail: `${percentage(connected, data.capabilities.length)}% of total` }), _jsx(ProjectUnderstandingMetric, { icon: MousePointerClick, label: "User-facing", value: String(userFacing), detail: `${percentage(userFacing, data.capabilities.length)}% of total` }), _jsx(ProjectUnderstandingMetric, { icon: Gauge, label: "Unresolved", value: String(unresolved), detail: "Requires review" })] }), _jsxs("div", { className: "anlyx-filter-row", "aria-label": "Filter capabilities by actor", children: [_jsx("span", { children: "Filter by" }), _jsx("button", { className: capabilityFilter === "all" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("all"), children: "All" }), _jsx("button", { className: capabilityFilter === "user" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("user"), children: "User" }), _jsx("button", { className: capabilityFilter === "entry" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("entry"), children: "Entry surface" }), _jsx("button", { className: capabilityFilter === "connected" ? "is-selected" : "", type: "button", onClick: () => setCapabilityFilter("connected"), children: "Connected only" })] }), data.capabilities.length === 0 ? (_jsx(ProjectSurfaceEmpty, { title: "No capabilities authored", description: "Add capabilities to describe product behavior without opening source code." })) : (_jsxs("div", { className: "anlyx-capability-table", role: "table", "aria-label": "Capabilities", children: [_jsxs("div", { className: "anlyx-capability-row is-header", role: "row", children: [_jsx("span", { children: "Actor" }), _jsx("span", { children: "Capability" }), _jsx("span", { children: "Entry surface" }), _jsx("span", { children: "Request" }), _jsx("span", { children: "Data touched" }), _jsx("span", { children: "Status" })] }), visibleCapabilities.map((capability) => (_jsxs("button", { className: `anlyx-capability-row${capability.id === selected?.id ? " is-selected" : ""}`, type: "button", onClick: () => setSelectedId(capability.id), children: [_jsxs("span", { className: `anlyx-role-badge is-${capability.actorRole}`, children: [_jsx("span", { className: `anlyx-role-dot is-${capability.actorRole}` }), capitalize(capability.actorRole)] }), _jsxs("span", { className: "anlyx-capability-name", children: [_jsx("strong", { children: capability.name }), _jsx("em", { children: capability.description ?? capability.visibleResult ?? "No capability description authored." })] }), _jsx("span", { children: capability.entry?.label ?? "No entry authored" }), _jsx("span", { children: requestSummary(data, capability.requestIds) }), _jsx("span", { children: capability.dataRefs.map((ref) => ref.name).join(", ") || "No data refs" }), _jsx(ProjectStatusPill, { status: capability.status })] }, capability.id))), _jsxs("div", { className: "anlyx-capability-footer", children: ["Showing ", visibleCapabilities.length, " of ", data.capabilities.length, " capabilities"] })] }))] }), _jsx("aside", { className: "anlyx-surface-rail", "aria-label": "Capability details", children: selected ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "anlyx-rail-section is-tinted", children: [_jsx("h2", { children: selected.name }), _jsx(ProjectStatusPill, { status: selected.status })] }), _jsxs("div", { className: "anlyx-rail-section", children: [_jsx("h3", { children: "Why this matters" }), _jsx("p", { children: capabilityWhyThisMatters(selected) })] }), _jsxs("div", { className: "anlyx-rail-section", children: [_jsx("h3", { children: "Trace summary" }), _jsxs("dl", { className: "anlyx-capability-compact-facts", children: [_jsxs("div", { children: [_jsx("dt", { children: "Actor" }), _jsx("dd", { children: capitalize(selected.actorRole) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Request" }), _jsx("dd", { children: requestSummary(data, selected.requestIds) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Confidence" }), _jsx("dd", { children: capitalize(selected.confidence ?? "unknown") })] })] })] }), _jsxs("div", { className: "anlyx-rail-section", children: [_jsx("h3", { children: "Evidence summary" }), _jsx(ProjectCapabilityEvidenceSummary, { capability: selected })] })] })) : (_jsx(ProjectSurfaceEmpty, { title: "No capability selected", description: "Select a capability row." })) })] }) }));
|
|
569
|
+
}
|
|
570
|
+
function ProjectCapabilityEvidenceSummary({ capability }) {
|
|
571
|
+
const pageCount = new Set(capability.pageIds).size;
|
|
572
|
+
const flowCount = new Set(capability.flowIds).size;
|
|
573
|
+
const dataCount = capability.dataRefs.length;
|
|
574
|
+
return (_jsxs("div", { className: "anlyx-evidence-summary-list", children: [_jsxs("span", { children: [_jsx(FileText, { size: 14 }), pageCount, " pages analyzed"] }), _jsxs("span", { children: [_jsx(Workflow, { size: 14 }), flowCount, " flows connected"] }), _jsxs("span", { children: [_jsx(Database, { size: 14 }), dataCount, " data objects referenced"] })] }));
|
|
575
|
+
}
|
|
576
|
+
function ProjectTrustSummary({ data, validationReport }) {
|
|
577
|
+
const coverage = projectPageCoverageSummary(data, validationReport);
|
|
578
|
+
const coverageStatus = validationReport?.summary.coverageStatus ?? data.coverage?.status;
|
|
579
|
+
const sourceIssueCount = validationReport?.summary.sourceIssueCount;
|
|
580
|
+
const sourceIssueDetails = validationReport
|
|
581
|
+
? formatSourceIssueDetails(validationReport.summary.sourceIssueBreakdown)
|
|
582
|
+
: "";
|
|
583
|
+
const issueCount = validationReport?.issues.length;
|
|
584
|
+
const shouldShow = Boolean(coverage.detected) ||
|
|
585
|
+
coverageStatus === "partial" ||
|
|
586
|
+
coverageStatus === "unknown" ||
|
|
587
|
+
(sourceIssueCount ?? 0) > 0 ||
|
|
588
|
+
(issueCount ?? 0) > 0;
|
|
589
|
+
if (!shouldShow) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return (_jsxs("section", { className: "project-trust-summary", "aria-label": "Project analysis coverage", children: [_jsxs("div", { children: [_jsx("span", { className: "project-trust-summary__eyebrow", children: "Analysis scope" }), _jsx("strong", { children: coverageStatus === "partial" ? "Partial analysis" : "Coverage summary" })] }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "Pages" }), _jsx("dd", { children: coverage.value })] }), sourceIssueCount !== undefined ? (_jsxs("div", { children: [_jsx("dt", { children: "Source issues" }), _jsxs("dd", { className: sourceIssueCount > 0 ? "is-warning" : "is-ok", children: [sourceIssueCount, sourceIssueDetails ? _jsx("small", { children: sourceIssueDetails }) : null] })] })) : null, issueCount !== undefined ? (_jsxs("div", { children: [_jsx("dt", { children: "Validation issues" }), _jsx("dd", { className: issueCount > 0 ? "is-warning" : "is-ok", children: issueCount })] })) : null] })] }));
|
|
593
|
+
}
|
|
594
|
+
function capabilityWhyThisMatters(capability) {
|
|
595
|
+
if (/inspect page behavior/i.test(capability.name)) {
|
|
596
|
+
return "This capability lets a user inspect authored pages, primary requests, selected flow layers, evidence, and unknowns—providing clarity into how data moves and where gaps exist.";
|
|
597
|
+
}
|
|
598
|
+
return capability.visibleResult ?? capability.description ?? "This capability ties a user-facing action to authored requests, data, evidence, and confidence.";
|
|
599
|
+
}
|
|
600
|
+
function ProjectUnderstandingMetric({ detail, icon: Icon, label, value }) {
|
|
601
|
+
return (_jsxs("div", { className: "anlyx-understanding-metric", children: [_jsx("span", { children: _jsx(Icon, { size: 20 }) }), _jsxs("div", { children: [_jsx("small", { children: label }), _jsx("strong", { children: value }), _jsx("em", { children: detail })] })] }));
|
|
602
|
+
}
|
|
603
|
+
function ProjectSurfaceEmpty({ description, title }) {
|
|
604
|
+
return (_jsxs("div", { className: "anlyx-surface-empty", children: [_jsx(Minus, { size: 18 }), _jsx("strong", { children: title }), _jsx("p", { children: description })] }));
|
|
605
|
+
}
|
|
606
|
+
function ProjectStatusPill({ status }) {
|
|
607
|
+
return _jsx("span", { className: `anlyx-status-pill is-${status}`, children: status });
|
|
608
|
+
}
|
|
609
|
+
function percentage(value, total) {
|
|
610
|
+
if (total <= 0)
|
|
611
|
+
return 0;
|
|
612
|
+
return Math.round((value / total) * 100);
|
|
613
|
+
}
|
|
614
|
+
function requestSummary(data, requestIds) {
|
|
615
|
+
const requests = requestIds
|
|
616
|
+
.map((id) => data.requests.find((request) => request.id === id))
|
|
617
|
+
.filter((request) => Boolean(request));
|
|
618
|
+
if (requests.length === 0)
|
|
619
|
+
return "No request authored";
|
|
620
|
+
return requests
|
|
621
|
+
.slice(0, 2)
|
|
622
|
+
.map((request) => `${request.method ?? "REQ"} ${request.path ?? request.label ?? request.id}`)
|
|
623
|
+
.join(", ");
|
|
624
|
+
}
|
|
625
|
+
function ProjectPagesWorkspace({ data, model, onOpenMap, onPageSelect, selectedPageId, validationReport }) {
|
|
626
|
+
const [isPageIndexCollapsed, setIsPageIndexCollapsed] = useState(false);
|
|
627
|
+
return (_jsxs("div", { className: `anlyx-pages project-pages-layout${isPageIndexCollapsed ? " is-index-collapsed" : ""}`, children: [_jsx(ProjectPageIndex, { data: data, isCollapsed: isPageIndexCollapsed, model: model, ...(validationReport ? { validationReport } : {}), selectedPageId: selectedPageId, onPageSelect: onPageSelect, onToggleCollapse: () => setIsPageIndexCollapsed((value) => !value) }), _jsx(ProjectPagesView, { data: data, model: model, ...(validationReport ? { validationReport } : {}), selectedPage: model.selectedPage, onOpenMap: onOpenMap })] }));
|
|
628
|
+
}
|
|
629
|
+
function ProjectPageIndex({ data, isCollapsed, model, onPageSelect, onToggleCollapse, selectedPageId, validationReport }) {
|
|
630
|
+
const pages = model.pageGroups.flatMap((group) => group.pages);
|
|
631
|
+
const pageCoverage = projectPageCoverageSummary(data, validationReport);
|
|
632
|
+
const [query, setQuery] = useState("");
|
|
633
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
634
|
+
const visibleGroups = normalizedQuery
|
|
635
|
+
? model.pageGroups
|
|
636
|
+
.map((group) => ({
|
|
637
|
+
...group,
|
|
638
|
+
pages: group.pages.filter((page) => page.title.toLowerCase().includes(normalizedQuery) ||
|
|
639
|
+
page.path.toLowerCase().includes(normalizedQuery))
|
|
640
|
+
}))
|
|
641
|
+
.filter((group) => group.pages.length > 0)
|
|
642
|
+
: model.pageGroups;
|
|
643
|
+
return (_jsxs("aside", { className: `anlyx-page-index project-page-index-panel${isCollapsed ? " is-collapsed" : ""}`, "aria-label": "Page index", children: [_jsxs("div", { className: "project-panel-title", children: [isCollapsed ? null : (_jsxs("div", { children: [_jsx("h2", { children: "Page Index" }), _jsx("span", { children: pageCoverage.label })] })), _jsx("button", { "aria-expanded": !isCollapsed, "aria-label": isCollapsed ? "Expand page index" : "Collapse page index", type: "button", onClick: onToggleCollapse, children: _jsx(PanelLeft, { size: 15 }) })] }), isCollapsed ? (_jsx("nav", { className: "project-page-index--collapsed", "aria-label": "Project pages", children: pages.map((page) => (_jsx("button", { "aria-label": `${page.title} ${page.path}`, className: selectedPageId === page.id ? "is-active" : "", title: `${page.title} ${page.path}`, type: "button", onClick: () => onPageSelect(page.id), children: _jsx(BookOpen, { size: 16 }) }, page.id))) })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "project-page-index__search", children: [_jsx(Search, { size: 16 }), _jsx("input", { "aria-label": "Search pages", placeholder: "Search pages...", type: "search", value: query, onChange: (event) => setQuery(event.currentTarget.value) }), _jsx("kbd", { children: "/" })] }), _jsx("nav", { className: "project-page-index", "aria-label": "Project pages", children: visibleGroups.map((group) => (_jsxs("section", { children: [_jsxs("h2", { children: [_jsx("span", { children: group.name }), _jsx("span", { children: group.pages.length })] }), group.pages.map((page) => (_jsxs("button", { className: selectedPageId === page.id ? "is-active" : "", type: "button", onClick: () => onPageSelect(page.id), children: [_jsx(BookOpen, { size: 15 }), _jsx("span", { children: page.title }), _jsx("em", { children: page.path })] }, page.id)))] }, group.id))) }), _jsxs("div", { className: "project-page-index-panel__footer", children: [_jsx("span", { children: "Total pages" }), _jsx("strong", { children: model.totals.pages })] })] }))] }));
|
|
644
|
+
}
|
|
645
|
+
function ProjectPagesView({ data, model, onOpenMap, selectedPage, validationReport }) {
|
|
646
|
+
const defaultRequestId = getDefaultPageRequestId(selectedPage);
|
|
647
|
+
const [selectedRequestId, setSelectedRequestId] = useState(defaultRequestId);
|
|
648
|
+
const [isRailOpen, setIsRailOpen] = useState(false);
|
|
649
|
+
useEffect(() => {
|
|
650
|
+
setSelectedRequestId(defaultRequestId);
|
|
651
|
+
}, [defaultRequestId, selectedPage?.page.id]);
|
|
652
|
+
if (!selectedPage) {
|
|
653
|
+
return (_jsxs("section", { className: "project-workspace-preview__empty", children: [_jsx("strong", { children: "No pages authored yet" }), _jsx("span", { children: "Ask the project Agent to write pages into anlyx.project.json." })] }));
|
|
654
|
+
}
|
|
655
|
+
const selectedRequest = selectedPage.requests.find((request) => request.id === selectedRequestId) ??
|
|
656
|
+
getDefaultPageRequest(selectedPage);
|
|
657
|
+
const selectedRequestLabel = selectedRequest?.path ?? selectedRequest?.label ?? selectedRequest?.id ?? "selected request";
|
|
658
|
+
const requestGroups = groupRequestsByRole(selectedPage.requests);
|
|
659
|
+
const selectedRequestFlows = selectedRequest
|
|
660
|
+
? selectedPage.flows.filter((flow) => flow.request?.id === selectedRequest.id ||
|
|
661
|
+
(flow.request?.path && flow.request.path === selectedRequest.path))
|
|
662
|
+
: [];
|
|
663
|
+
const visibleFlows = selectedRequest ? selectedRequestFlows : selectedPage.flows;
|
|
664
|
+
const unknownEvidence = selectedPage.evidence.filter((item) => item.status === "not-proven" || item.status === "unknown");
|
|
665
|
+
const selectedFlow = visibleFlows[0];
|
|
666
|
+
return (_jsxs("div", { className: `project-page-workspace${isRailOpen ? "" : " is-rail-collapsed"}`, children: [_jsxs("main", { className: "anlyx-page-report project-page-workspace__main", children: [!isRailOpen ? (_jsxs("button", { className: "anlyx-rail-reopen", type: "button", onClick: () => setIsRailOpen(true), children: [_jsx(PanelLeft, { size: 14 }), "Details"] })) : null, _jsx(ProjectTrustSummary, { data: data, ...(validationReport ? { validationReport } : {}) }), _jsx(PageBrief, { selectedPage: selectedPage }), _jsxs("div", { className: "project-page-two-up", children: [_jsx(ProjectSection, { title: "Story", children: _jsx("p", { className: "project-story-copy", children: selectedPage.page.description ??
|
|
667
|
+
"No story was authored for this page yet. Ask the project Agent to describe what this page does and why it exists." }) }), _jsx(ProjectSection, { className: "anlyx-user-actions", title: "User actions", children: _jsx("div", { className: "anlyx-action-list project-action-list", children: selectedPage.features.length > 0 ? (selectedPage.features.map((feature) => (_jsx(ProjectFeatureCard, { feature: feature }, feature.id)))) : (_jsx("p", { className: "project-muted", children: "No user actions authored for this page." })) }) })] }), _jsx(RequestsByRole, { groups: requestGroups, selectedRequest: selectedRequest, onRequestSelect: setSelectedRequestId }), _jsx(ProjectSection, { className: "anlyx-flow-section", meta: selectedRequest
|
|
668
|
+
? `${selectedRequest.method ?? "HTTP"} ${selectedRequestLabel}`
|
|
669
|
+
: undefined, title: "Selected Flow", children: selectedFlow ? (_jsx(ProjectFlowTrace, { flow: selectedFlow })) : (_jsx("p", { className: "project-muted", children: "No backend flow linked to this request." })) }), _jsxs("div", { className: "anlyx-trust-unknowns project-page-two-up project-page-two-up--trust", children: [_jsx(ProjectSection, { className: "anlyx-trust-breakdown", title: "Trust Breakdown", children: _jsxs("div", { className: "anlyx-trust-grid project-evidence-row", children: [_jsx(EvidenceChip, { label: "Source-matched", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.sourceMatched }), _jsx(EvidenceChip, { label: "Agent-inferred", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.agentInferred }), _jsx(EvidenceChip, { label: "Observed", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.observed }), _jsx(EvidenceChip, { label: "Not-proven", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.notProven }), _jsx(EvidenceChip, { label: "Unknown", total: selectedPage.evidenceSummary.total, value: selectedPage.evidenceSummary.unknown })] }) }), _jsx(ProjectSection, { className: "anlyx-unknowns", title: "Unknowns", children: unknownEvidence.length > 0 ? (_jsx("ul", { className: "anlyx-unknown-list project-unknown-list", children: unknownEvidence.slice(0, 4).map((item) => (_jsx("li", { children: item.detail ?? item.label }, item.id))) })) : (_jsxs("div", { className: "anlyx-unknown-empty", children: [_jsx("p", { children: "No unknown or not-proven evidence authored for this page." }), _jsx("p", { children: "All detected elements are source-matched." })] })) })] })] }), isRailOpen ? (_jsx(PageDetailsRail, { model: model, selectedPage: selectedPage, onCollapse: () => setIsRailOpen(false), onOpenMap: onOpenMap })) : null] }));
|
|
670
|
+
}
|
|
671
|
+
function PageBrief({ selectedPage }) {
|
|
672
|
+
return (_jsx("header", { className: "project-page-brief", children: _jsx("div", { className: "project-page-brief__body", children: _jsxs("div", { className: "project-page-brief__title", children: [_jsxs("div", { children: [_jsx("h1", { children: selectedPage.page.title }), _jsx("code", { children: selectedPage.page.path })] }), _jsx("p", { children: selectedPage.page.description ??
|
|
673
|
+
"No page story was authored yet. Ask the project Agent to describe what this page does and why it exists." })] }) }) }));
|
|
674
|
+
}
|
|
675
|
+
function RequestsByRole({ groups, onRequestSelect, selectedRequest }) {
|
|
676
|
+
return (_jsx(ProjectSection, { title: "Requests", children: _jsx("div", { className: "anlyx-requests-grid project-request-columns", children: groups.some((group) => group.requests.length > 0) ? (groups.map((group) => (_jsxs("section", { className: "project-request-column", children: [_jsxs("header", { children: [_jsx("strong", { children: group.label }), _jsx("span", { children: group.requests.length })] }), group.requests.length > 0 ? (group.requests.map((request) => (_jsx(ProjectRequestCard, { isSelected: selectedRequest?.id === request.id, request: request, onSelect: () => onRequestSelect(request.id) }, request.id)))) : (_jsxs("div", { className: "anlyx-request-empty", children: [_jsx(Minus, { size: 18 }), _jsxs("strong", { children: ["No ", group.label.toLowerCase(), " requests"] }), _jsx("span", { children: "None detected for this page." })] }))] }, group.role)))) : (_jsx("p", { className: "project-muted", children: "No requests linked to this page." })) }) }));
|
|
677
|
+
}
|
|
678
|
+
function getDefaultPageRequestId(selectedPage) {
|
|
679
|
+
return getDefaultPageRequest(selectedPage)?.id;
|
|
680
|
+
}
|
|
681
|
+
function getDefaultPageRequest(selectedPage) {
|
|
682
|
+
if (!selectedPage)
|
|
683
|
+
return undefined;
|
|
684
|
+
return (selectedPage.requests.find((request) => request.role === "primary") ??
|
|
685
|
+
selectedPage.requests.find((request) => request.role === "supporting") ??
|
|
686
|
+
selectedPage.requests[0]);
|
|
687
|
+
}
|
|
688
|
+
function groupRequestsByRole(requests) {
|
|
689
|
+
const groups = [
|
|
690
|
+
{ role: "primary", label: "Primary", requests: [] },
|
|
691
|
+
{ role: "supporting", label: "Supporting", requests: [] },
|
|
692
|
+
{ role: "background", label: "Background", requests: [] },
|
|
693
|
+
{ role: "external", label: "External", requests: [] }
|
|
694
|
+
];
|
|
695
|
+
for (const request of requests) {
|
|
696
|
+
const group = groups.find((item) => item.role === request.role);
|
|
697
|
+
group?.requests.push(request);
|
|
698
|
+
}
|
|
699
|
+
const externalGroup = groups.find((group) => group.role === "external");
|
|
700
|
+
const baseGroups = groups.filter((group) => group.role !== "external");
|
|
701
|
+
return externalGroup && externalGroup.requests.length > 0
|
|
702
|
+
? [...baseGroups, externalGroup]
|
|
703
|
+
: baseGroups;
|
|
704
|
+
}
|
|
705
|
+
function ProjectSection({ children, className, index, meta, title }) {
|
|
706
|
+
return (_jsxs("section", { className: `anlyx-report-section project-section${className ? ` ${className}` : ""}`, children: [_jsxs("header", { className: "project-section__header", children: [_jsxs("h3", { children: [index ? (_jsx("span", { className: "project-section__number", "aria-hidden": "true", children: index })) : null, _jsx("span", { children: title })] }), meta ? _jsx("span", { children: meta }) : null] }), children] }));
|
|
707
|
+
}
|
|
708
|
+
function ProjectFeatureCard({ feature }) {
|
|
709
|
+
return (_jsx("article", { className: "anlyx-action-item project-feature-card", children: _jsxs("div", { children: [_jsx("strong", { children: feature.name }), _jsx("p", { children: feature.description ?? "No feature description authored yet." })] }) }));
|
|
710
|
+
}
|
|
711
|
+
function ProjectRequestCard({ isSelected, onSelect, request }) {
|
|
712
|
+
const label = request.path ?? request.label ?? request.id;
|
|
713
|
+
return (_jsxs("button", { className: `anlyx-request-card project-request-card project-request-card--${request.role} ${isSelected ? "is-selected" : ""}`, type: "button", onClick: onSelect, children: [_jsxs("header", { children: [_jsx("span", { className: `project-method-badge project-method-badge--${request.method ?? "HTTP"}`, children: request.method ?? "HTTP" }), isSelected ? _jsx(Check, { size: 15 }) : null] }), _jsx("strong", { children: label }), _jsx("p", { children: request.description ?? request.purpose }), _jsx("footer", { children: _jsx("b", { children: request.role }) })] }));
|
|
714
|
+
}
|
|
715
|
+
function ProjectFlowTrace({ flow }) {
|
|
716
|
+
const layers = flow.layers.length > 0 ? flow.layers : [];
|
|
717
|
+
const [expandedLayerId, setExpandedLayerId] = useState();
|
|
718
|
+
return (_jsxs("article", { className: "anlyx-flow-trace project-flow-trace", children: [_jsxs("header", { className: "project-flow-trace__header", children: [_jsx("span", { children: flow.request?.role ?? "flow" }), _jsx("strong", { children: flow.name ?? flow.request?.path ?? flow.id })] }), _jsx("div", { className: "anlyx-flow-scroll", children: _jsx("div", { className: "anlyx-flow-track project-flow-trace__layers", children: layers.map((layer, index) => (_jsxs(Fragment, { children: [_jsx(ProjectFlowLayerCard, { isExpanded: expandedLayerId === layer.id, layer: layer, onToggle: () => setExpandedLayerId((current) => (current === layer.id ? undefined : layer.id)) }), index < layers.length - 1 ? (_jsx("span", { className: "anlyx-flow-arrow", "aria-hidden": "true", children: _jsx(ChevronRight, { size: 18 }) })) : null] }, layer.id))) }) })] }));
|
|
719
|
+
}
|
|
720
|
+
function ProjectFlowLayerCard({ isExpanded, onToggle, layer }) {
|
|
721
|
+
const source = formatSourceLocation(layer.source) ?? formatLayerHint(layer);
|
|
722
|
+
return (_jsxs("button", { "aria-expanded": isExpanded, className: `anlyx-flow-node project-flow-layer project-flow-layer--${layer.kind}${isExpanded ? " is-expanded" : ""}`, title: source, type: "button", onClick: onToggle, children: [_jsx("span", { className: "anlyx-flow-layer", children: titleCase(layer.kind) }), _jsx("strong", { className: "anlyx-flow-title", children: layer.label }), _jsx("em", { className: "anlyx-flow-source", children: source }), _jsx("small", { className: "anlyx-evidence-pill", children: evidenceStatusLabel(layer.status) })] }));
|
|
723
|
+
}
|
|
724
|
+
function EvidenceChip({ label, total, value }) {
|
|
725
|
+
const percent = total > 0 ? Math.round((value / total) * 100) : 0;
|
|
726
|
+
const status = label.toLowerCase().replace(/[^a-z]+/g, "-").replace(/^-|-$/g, "");
|
|
727
|
+
return (_jsxs("span", { className: `anlyx-trust-card anlyx-trust-card--${status} project-evidence-chip`, children: [_jsxs("span", { children: [_jsx("i", { "aria-hidden": "true" }), label] }), _jsx("strong", { children: value }), _jsxs("em", { children: [percent, "%"] })] }));
|
|
728
|
+
}
|
|
729
|
+
function PageDetailsRail({ model, onCollapse, onOpenMap, selectedPage }) {
|
|
730
|
+
const page = selectedPage.page;
|
|
731
|
+
const relatedPages = getRelatedPages(model, page.metadata?.["relatedPageIds"]);
|
|
732
|
+
const tags = getStringList(page.metadata?.["tags"]);
|
|
733
|
+
const evidenceLabel = formatEvidenceSummary(selectedPage.evidenceSummary);
|
|
734
|
+
return (_jsxs("aside", { className: "anlyx-page-rail project-page-details-panel", "aria-label": "Page details", children: [_jsxs("div", { className: "anlyx-page-rail__header", children: [_jsx("h2", { children: "Page Details" }), _jsx("button", { "aria-label": "Collapse page details", type: "button", onClick: onCollapse, children: _jsx(PanelLeft, { size: 14 }) })] }), _jsxs("dl", { children: [_jsx(ProjectDetailField, { label: "Area", value: selectedPage.areaName }), _jsx(ProjectDetailField, { label: "Route", value: page.path }), _jsx(ProjectDetailField, { label: "Page Type", value: metadataString(page.metadata?.["pageType"]) ?? "Not authored" }), _jsx(ProjectDetailField, { label: "Auth Required", value: metadataBooleanLabel(page.metadata?.["authRequired"]) }), _jsx(ProjectDetailField, { label: "Layout", value: metadataString(page.metadata?.["layout"]) ?? "Not authored" }), _jsx(ProjectDetailField, { label: "Last Seen", value: formatAnalysisTime(model.project.analyzedAt) ?? "Not authored" })] }), _jsxs("section", { children: [_jsx("h3", { children: "Evidence" }), _jsx("p", { children: evidenceLabel }), page.source ? _jsx("p", { children: formatSourceLocation(page.source) }) : null] }), _jsxs("section", { children: [_jsx("h3", { children: "Related Pages" }), relatedPages.length > 0 ? (_jsx("ul", { children: relatedPages.map((relatedPage) => (_jsxs("li", { children: [_jsx("strong", { children: relatedPage.title }), _jsx("span", { children: relatedPage.path })] }, relatedPage.id))) })) : (_jsx("p", { children: "No related pages authored." }))] }), _jsxs("section", { children: [_jsx("h3", { children: "Tags" }), tags.length > 0 ? (_jsx("div", { className: "project-page-tags", children: tags.map((tag) => (_jsxs("span", { children: [_jsx(Tag, { size: 12 }), tag] }, tag))) })) : (_jsx("p", { children: "No tags authored." }))] }), _jsxs("button", { type: "button", onClick: onOpenMap, children: [_jsx(Network, { size: 15 }), "View in Map"] })] }));
|
|
735
|
+
}
|
|
736
|
+
function ProjectDetailField({ label, value }) {
|
|
737
|
+
return (_jsxs("div", { children: [_jsx("dt", { children: label }), _jsx("dd", { children: value })] }));
|
|
738
|
+
}
|
|
739
|
+
function getRelatedPages(model, value) {
|
|
740
|
+
const ids = getStringList(value);
|
|
741
|
+
const pages = model.pageGroups.flatMap((group) => group.pages);
|
|
742
|
+
return ids.flatMap((id) => {
|
|
743
|
+
const page = pages.find((item) => item.id === id);
|
|
744
|
+
return page ? [{ id: page.id, path: page.path, title: page.title }] : [];
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function getStringList(value) {
|
|
748
|
+
return Array.isArray(value)
|
|
749
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0)
|
|
750
|
+
: [];
|
|
751
|
+
}
|
|
752
|
+
function metadataString(value) {
|
|
753
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
754
|
+
}
|
|
755
|
+
function metadataBooleanLabel(value) {
|
|
756
|
+
if (typeof value === "boolean")
|
|
757
|
+
return value ? "Yes" : "No";
|
|
758
|
+
if (typeof value === "string" && value.trim().length > 0)
|
|
759
|
+
return value;
|
|
760
|
+
return "Not authored";
|
|
761
|
+
}
|
|
762
|
+
function formatAnalysisTime(value) {
|
|
763
|
+
if (!value)
|
|
764
|
+
return undefined;
|
|
765
|
+
const date = new Date(value);
|
|
766
|
+
if (Number.isNaN(date.getTime()))
|
|
767
|
+
return value;
|
|
768
|
+
return date.toLocaleString("en", {
|
|
769
|
+
day: "2-digit",
|
|
770
|
+
hour: "2-digit",
|
|
771
|
+
minute: "2-digit",
|
|
772
|
+
month: "short",
|
|
773
|
+
year: "numeric"
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
function formatEvidenceSummary(summary) {
|
|
777
|
+
if (summary.total === 0)
|
|
778
|
+
return "Not authored";
|
|
779
|
+
const strongest = summary.sourceMatched > 0
|
|
780
|
+
? "Source-matched"
|
|
781
|
+
: summary.observed > 0
|
|
782
|
+
? "Observed"
|
|
783
|
+
: summary.agentInferred > 0
|
|
784
|
+
? "Agent-inferred"
|
|
785
|
+
: summary.notProven > 0
|
|
786
|
+
? "Not-proven"
|
|
787
|
+
: "Unknown";
|
|
788
|
+
return `${strongest} (${summary.total})`;
|
|
789
|
+
}
|
|
790
|
+
function formatSourceLocation(source) {
|
|
791
|
+
if (!source)
|
|
792
|
+
return undefined;
|
|
793
|
+
const line = source.lineStart ? `:${source.lineStart}` : "";
|
|
794
|
+
const symbol = source.symbol ? ` ${source.symbol}` : "";
|
|
795
|
+
return `${source.filePath}${line}${symbol}`;
|
|
796
|
+
}
|
|
797
|
+
function formatLayerHint(layer) {
|
|
798
|
+
return metadataString(layer.metadata?.["module"]) ?? metadataString(layer.metadata?.["file"]) ?? layer.id;
|
|
799
|
+
}
|
|
800
|
+
function titleCase(value) {
|
|
801
|
+
return value
|
|
802
|
+
.split("-")
|
|
803
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`)
|
|
804
|
+
.join("-");
|
|
805
|
+
}
|
|
806
|
+
function ProjectMapView({ data, model }) {
|
|
807
|
+
const map = useMemo(() => buildProjectArchitectureMap(data), [data]);
|
|
808
|
+
const [selectedNodeId, setSelectedNodeId] = useState(undefined);
|
|
809
|
+
const [inspectorOpen, setInspectorOpen] = useState(false);
|
|
810
|
+
const selectedNode = map.nodes.find((node) => node.id === selectedNodeId);
|
|
811
|
+
const focus = useMemo(() => buildProjectArchitectureFocus(map, selectedNode?.id), [map, selectedNode?.id]);
|
|
812
|
+
const selectNode = (nodeId) => {
|
|
813
|
+
setSelectedNodeId(nodeId);
|
|
814
|
+
setInspectorOpen(true);
|
|
815
|
+
};
|
|
816
|
+
if (data.architecture.nodes.length === 0) {
|
|
817
|
+
return (_jsxs("section", { className: "project-map-placeholder", "aria-label": "Project architecture map", children: [_jsxs("div", { children: [_jsx(Network, { size: 24 }), _jsx("h2", { children: "Map" }), _jsx("p", { children: "No architecture graph was authored yet. The viewer will not invent mock project data." })] }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "Nodes" }), _jsx("dd", { children: "0" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Edges" }), _jsx("dd", { children: "0" })] })] })] }));
|
|
818
|
+
}
|
|
819
|
+
return (_jsxs("section", { className: "project-architecture-map anlyx-map-shell", "aria-label": "Project architecture map", children: [_jsxs("header", { className: "project-architecture-map__toolbar", children: [_jsxs("div", { children: [_jsx("h1", { children: "Map" }), _jsx("p", { children: "Agent-authored architecture map. The viewer renders nodes and edges from project JSON." })] }), _jsx("div", { className: "anlyx-map-header-actions", children: _jsx(ProjectArchitectureMapStats, { map: map, model: model }) })] }), _jsxs("div", { className: `anlyx-map-body${inspectorOpen ? " has-inspector" : ""}`, children: [_jsx("div", { className: `project-architecture-map__canvas${selectedNode ? " is-focused" : ""}${map.isCompact ? " is-small-graph" : ""}`, "data-testid": "project-architecture-map", style: {
|
|
820
|
+
"--anlyx-map-width": `${map.width}px`,
|
|
821
|
+
"--anlyx-map-height": `${map.height}px`
|
|
822
|
+
}, children: _jsxs("div", { className: "anlyx-map-stage", children: [_jsx("svg", { "aria-hidden": "true", className: "anlyx-map-edges", height: map.height, viewBox: `0 0 ${map.width} ${map.height}`, width: map.width, children: map.edges.map((edge) => (_jsx(ProjectArchitectureMapEdge, { edge: edge, focus: focus }, edge.id))) }), _jsx("div", { className: "anlyx-map-columns", "aria-hidden": "true", children: map.columns.map((column) => (_jsxs("div", { className: "anlyx-map-column-guide", style: { left: column.x, width: column.width }, children: [_jsx("strong", { children: column.label }), _jsx("span", { children: column.count })] }, column.id))) }), map.nodes.map((node) => (_jsx(ProjectArchitectureMapNode, { focus: focus, node: node, onSelect: selectNode, selected: node.id === selectedNode?.id }, node.id))), _jsxs("div", { className: "anlyx-map-legend", children: [_jsxs("span", { children: [_jsx("i", { className: "is-primary" }), " Selected path"] }), _jsxs("span", { children: [_jsx("i", { className: "is-shared" }), " Shared dependency"] }), _jsxs("span", { children: [_jsx("i", { className: "is-muted" }), " Other / Unknown"] })] })] }) }), inspectorOpen ? (_jsx(ProjectArchitectureInspector, { focus: focus, node: selectedNode, nodesById: map.nodesById, onClose: () => {
|
|
823
|
+
setInspectorOpen(false);
|
|
824
|
+
setSelectedNodeId(undefined);
|
|
825
|
+
} })) : null] })] }));
|
|
826
|
+
}
|
|
827
|
+
const projectArchitectureColumns = [
|
|
828
|
+
{ id: "pages", label: "Pages / Features", kinds: ["frontend"] },
|
|
829
|
+
{ id: "requests", label: "Requests", kinds: ["request"] },
|
|
830
|
+
{ id: "api", label: "API", kinds: ["api"] },
|
|
831
|
+
{ id: "controllers", label: "Controllers", kinds: ["controller", "handler", "middleware"] },
|
|
832
|
+
{ id: "services", label: "Services", kinds: ["service", "cache", "queue", "job", "external"] },
|
|
833
|
+
{ id: "repository", label: "Repository / Mapper", kinds: ["repository", "mapper"] },
|
|
834
|
+
{ id: "policy", label: "Policy / Schema", kinds: ["policy"] },
|
|
835
|
+
{ id: "data", label: "Data / JSON", kinds: ["database"] },
|
|
836
|
+
{ id: "result", label: "Results", kinds: ["result", "unknown"] }
|
|
837
|
+
];
|
|
838
|
+
function buildProjectArchitectureMap(data) {
|
|
839
|
+
const nodeIds = new Set(data.architecture.nodes.map((node) => node.id));
|
|
840
|
+
const validEdges = data.architecture.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target));
|
|
841
|
+
const evidenceById = new Map(data.evidence.map((evidence) => [evidence.id, evidence]));
|
|
842
|
+
const connectionCounts = new Map();
|
|
843
|
+
const upstreamCounts = new Map();
|
|
844
|
+
const downstreamCounts = new Map();
|
|
845
|
+
validEdges.forEach((edge) => {
|
|
846
|
+
connectionCounts.set(edge.source, (connectionCounts.get(edge.source) ?? 0) + 1);
|
|
847
|
+
connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
|
|
848
|
+
downstreamCounts.set(edge.source, (downstreamCounts.get(edge.source) ?? 0) + 1);
|
|
849
|
+
upstreamCounts.set(edge.target, (upstreamCounts.get(edge.target) ?? 0) + 1);
|
|
850
|
+
});
|
|
851
|
+
const columnIdByKind = new Map(projectArchitectureColumns.flatMap((column) => column.kinds.map((kind) => [kind, column.id])));
|
|
852
|
+
const isCompact = data.architecture.nodes.length <= 12;
|
|
853
|
+
const filledColumnWidth = isCompact ? 176 : 196;
|
|
854
|
+
const nodeHeight = isCompact ? 90 : 96;
|
|
855
|
+
const columnGap = isCompact ? 28 : 34;
|
|
856
|
+
const rowGap = isCompact ? 16 : 22;
|
|
857
|
+
const top = isCompact ? 62 : 82;
|
|
858
|
+
const left = 18;
|
|
859
|
+
const grouped = new Map();
|
|
860
|
+
for (const node of data.architecture.nodes) {
|
|
861
|
+
const columnId = columnIdByKind.get(node.kind) ?? "result";
|
|
862
|
+
const nodes = grouped.get(columnId) ?? [];
|
|
863
|
+
nodes.push(node);
|
|
864
|
+
grouped.set(columnId, nodes);
|
|
865
|
+
}
|
|
866
|
+
const columnCounts = new Map();
|
|
867
|
+
for (const column of projectArchitectureColumns) {
|
|
868
|
+
columnCounts.set(column.id, grouped.get(column.id)?.length ?? 0);
|
|
869
|
+
}
|
|
870
|
+
let currentX = left;
|
|
871
|
+
const columns = projectArchitectureColumns
|
|
872
|
+
.filter((column) => (columnCounts.get(column.id) ?? 0) > 0)
|
|
873
|
+
.map((column) => {
|
|
874
|
+
const count = columnCounts.get(column.id) ?? 0;
|
|
875
|
+
const width = filledColumnWidth;
|
|
876
|
+
const positionedColumn = {
|
|
877
|
+
...column,
|
|
878
|
+
x: currentX,
|
|
879
|
+
width,
|
|
880
|
+
count
|
|
881
|
+
};
|
|
882
|
+
currentX += width + columnGap;
|
|
883
|
+
return positionedColumn;
|
|
884
|
+
});
|
|
885
|
+
const mapNodes = [];
|
|
886
|
+
for (const column of columns) {
|
|
887
|
+
const nodes = grouped.get(column.id) ?? [];
|
|
888
|
+
nodes
|
|
889
|
+
.sort((a, b) => {
|
|
890
|
+
const degreeDelta = (connectionCounts.get(b.id) ?? 0) - (connectionCounts.get(a.id) ?? 0);
|
|
891
|
+
return degreeDelta || a.label.localeCompare(b.label);
|
|
892
|
+
})
|
|
893
|
+
.forEach((node, index) => {
|
|
894
|
+
const evidenceStatus = projectArchitectureEvidenceStatus(node.evidenceIds, evidenceById);
|
|
895
|
+
const source = formatProjectArchitectureSource(node.source);
|
|
896
|
+
mapNodes.push({
|
|
897
|
+
...node,
|
|
898
|
+
columnId: column.id,
|
|
899
|
+
x: column.x,
|
|
900
|
+
y: top + index * (nodeHeight + rowGap),
|
|
901
|
+
width: column.width,
|
|
902
|
+
height: nodeHeight,
|
|
903
|
+
connectionCount: connectionCounts.get(node.id) ?? 0,
|
|
904
|
+
upstreamCount: upstreamCounts.get(node.id) ?? 0,
|
|
905
|
+
downstreamCount: downstreamCounts.get(node.id) ?? 0,
|
|
906
|
+
evidenceStatus,
|
|
907
|
+
evidenceLabel: node.evidenceIds.length > 0 ? evidenceStatusLabel(evidenceStatus) : "Unknown",
|
|
908
|
+
subtitle: source ?? projectArchitectureNodeSubtitle(node)
|
|
909
|
+
});
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
const nodesById = new Map(mapNodes.map((node) => [node.id, node]));
|
|
913
|
+
const logicalEdges = validEdges
|
|
914
|
+
.map((edge) => {
|
|
915
|
+
const sourceNode = nodesById.get(edge.source);
|
|
916
|
+
const targetNode = nodesById.get(edge.target);
|
|
917
|
+
if (!sourceNode || !targetNode)
|
|
918
|
+
return undefined;
|
|
919
|
+
return sourceNode && targetNode ? { ...edge, sourceNode, targetNode, routeX: 0 } : undefined;
|
|
920
|
+
})
|
|
921
|
+
.filter((edge) => Boolean(edge));
|
|
922
|
+
const mapEdgesWithoutLanes = logicalEdges.filter((edge) => {
|
|
923
|
+
if (edge.sourceNode.columnId === edge.targetNode.columnId)
|
|
924
|
+
return false;
|
|
925
|
+
return edge.sourceNode.x + edge.sourceNode.width < edge.targetNode.x;
|
|
926
|
+
});
|
|
927
|
+
const gutterCounts = new Map();
|
|
928
|
+
const mapEdges = mapEdgesWithoutLanes.map((edge) => {
|
|
929
|
+
const sourceRight = edge.sourceNode.x + edge.sourceNode.width;
|
|
930
|
+
const gap = Math.max(1, edge.targetNode.x - sourceRight);
|
|
931
|
+
const gutterKey = `${edge.sourceNode.columnId}->${edge.targetNode.columnId}`;
|
|
932
|
+
const gutterIndex = gutterCounts.get(gutterKey) ?? 0;
|
|
933
|
+
gutterCounts.set(gutterKey, gutterIndex + 1);
|
|
934
|
+
const laneOffset = (gutterIndex - 1) * 6;
|
|
935
|
+
const routeX = sourceRight + gap / 2 + laneOffset;
|
|
936
|
+
return {
|
|
937
|
+
...edge,
|
|
938
|
+
routeX: Math.max(sourceRight + 14, Math.min(edge.targetNode.x - 14, routeX))
|
|
939
|
+
};
|
|
940
|
+
});
|
|
941
|
+
const maxColumnCount = Math.max(1, ...columns.map((column) => column.count));
|
|
942
|
+
return {
|
|
943
|
+
columns,
|
|
944
|
+
columnCounts,
|
|
945
|
+
nodes: mapNodes,
|
|
946
|
+
edges: mapEdges,
|
|
947
|
+
logicalEdges,
|
|
948
|
+
nodesById,
|
|
949
|
+
width: Math.max(0, currentX - columnGap + left),
|
|
950
|
+
height: top + maxColumnCount * (nodeHeight + rowGap) + (isCompact ? 34 : 80),
|
|
951
|
+
nodeWidth: filledColumnWidth,
|
|
952
|
+
isCompact
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function buildProjectArchitectureFocus(map, selectedNodeId) {
|
|
956
|
+
if (!selectedNodeId) {
|
|
957
|
+
return { nodeIds: new Set(), edgeIds: new Set(), upstreamIds: [], downstreamIds: [] };
|
|
958
|
+
}
|
|
959
|
+
const upstreamIds = collectArchitectureReachable(map.logicalEdges, selectedNodeId, "upstream");
|
|
960
|
+
const downstreamIds = collectArchitectureReachable(map.logicalEdges, selectedNodeId, "downstream");
|
|
961
|
+
const nodeIds = new Set([selectedNodeId, ...upstreamIds, ...downstreamIds]);
|
|
962
|
+
const edgeIds = new Set(map.logicalEdges
|
|
963
|
+
.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target))
|
|
964
|
+
.map((edge) => edge.id));
|
|
965
|
+
return { nodeIds, edgeIds, upstreamIds, downstreamIds };
|
|
966
|
+
}
|
|
967
|
+
function collectArchitectureReachable(edges, nodeId, direction) {
|
|
968
|
+
const result = [];
|
|
969
|
+
const visited = new Set([nodeId]);
|
|
970
|
+
const queue = [nodeId];
|
|
971
|
+
while (queue.length > 0) {
|
|
972
|
+
const current = queue.shift() ?? "";
|
|
973
|
+
const nextEdges = direction === "downstream"
|
|
974
|
+
? edges.filter((edge) => edge.source === current)
|
|
975
|
+
: edges.filter((edge) => edge.target === current);
|
|
976
|
+
for (const edge of nextEdges) {
|
|
977
|
+
const nextId = direction === "downstream" ? edge.target : edge.source;
|
|
978
|
+
if (visited.has(nextId))
|
|
979
|
+
continue;
|
|
980
|
+
visited.add(nextId);
|
|
981
|
+
result.push(nextId);
|
|
982
|
+
queue.push(nextId);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
return result;
|
|
986
|
+
}
|
|
987
|
+
function ProjectArchitectureMapStats({ map, model }) {
|
|
988
|
+
const stats = [
|
|
989
|
+
{ label: "Pages", value: model.totals.pages },
|
|
990
|
+
{ label: "Requests", value: map.columnCounts.get("requests") ?? 0 },
|
|
991
|
+
{ label: "API", value: map.columnCounts.get("api") ?? 0 },
|
|
992
|
+
{ label: "Controllers", value: map.columnCounts.get("controllers") ?? 0 },
|
|
993
|
+
{ label: "Services", value: map.columnCounts.get("services") ?? 0 },
|
|
994
|
+
{ label: "Mappers", value: map.columnCounts.get("repository") ?? 0 },
|
|
995
|
+
{ label: "Results", value: map.columnCounts.get("result") ?? 0 }
|
|
996
|
+
];
|
|
997
|
+
return (_jsx("dl", { children: stats.map((stat) => (_jsxs("div", { children: [_jsx("dt", { children: stat.label }), _jsx("dd", { children: stat.value })] }, stat.label))) }));
|
|
998
|
+
}
|
|
999
|
+
function ProjectArchitectureMapNode({ focus, node, onSelect, selected }) {
|
|
1000
|
+
const Icon = projectArchitectureNodeIcon(node.kind);
|
|
1001
|
+
const muted = focus.nodeIds.size > 0 && !focus.nodeIds.has(node.id);
|
|
1002
|
+
const shared = node.upstreamCount > 1 || node.downstreamCount > 1;
|
|
1003
|
+
return (_jsxs("button", { className: `anlyx-map-node${selected ? " is-selected" : ""}${muted ? " is-muted" : ""}`, onClick: () => onSelect(node.id), style: { left: node.x, top: node.y, width: node.width, minHeight: node.height }, title: `${node.label}${node.source?.filePath ? ` · ${node.source.filePath}` : ""}`, type: "button", children: [_jsx("span", { className: "anlyx-map-node__icon", "aria-hidden": "true", children: _jsx(Icon, { size: 15 }) }), _jsxs("span", { className: "anlyx-map-node__body", children: [_jsx("span", { className: "anlyx-map-node__layer", children: projectArchitectureKindLabel(node.kind) }), _jsx("strong", { children: node.displayLabel ?? node.label }), _jsx("small", { children: node.subtitle })] }), _jsx("span", { className: `anlyx-map-evidence anlyx-map-evidence--${node.evidenceStatus}`, children: node.evidenceLabel }), shared ? _jsx("span", { className: "anlyx-map-node__degree", children: node.connectionCount }) : null] }));
|
|
1004
|
+
}
|
|
1005
|
+
function ProjectArchitectureMapEdge({ edge, focus }) {
|
|
1006
|
+
const selected = focus.edgeIds.has(edge.id);
|
|
1007
|
+
const muted = focus.edgeIds.size > 0 && !selected;
|
|
1008
|
+
const sourceX = edge.sourceNode.x + edge.sourceNode.width;
|
|
1009
|
+
const sourceY = edge.sourceNode.y + edge.sourceNode.height / 2;
|
|
1010
|
+
const targetX = edge.targetNode.x;
|
|
1011
|
+
const targetY = edge.targetNode.y + edge.targetNode.height / 2;
|
|
1012
|
+
const path = createProjectArchitectureOrthogonalPath(sourceX, sourceY, edge.routeX, targetX, targetY);
|
|
1013
|
+
const role = edge.role ?? "primary";
|
|
1014
|
+
const status = projectArchitectureEdgeStatus(edge);
|
|
1015
|
+
const visualRole = status === "not-proven" || status === "unknown"
|
|
1016
|
+
? "unknown"
|
|
1017
|
+
: role === "shared" || edge.targetNode.upstreamCount > 1
|
|
1018
|
+
? "shared"
|
|
1019
|
+
: "primary";
|
|
1020
|
+
return (_jsx("path", { className: `anlyx-map-edge anlyx-map-edge--${visualRole}${selected ? " is-selected" : ""}${muted ? " is-muted" : ""}`, d: path }));
|
|
1021
|
+
}
|
|
1022
|
+
function createProjectArchitectureOrthogonalPath(sourceX, sourceY, routeX, targetX, targetY) {
|
|
1023
|
+
return `M ${sourceX} ${sourceY} H ${routeX} V ${targetY} H ${targetX}`;
|
|
1024
|
+
}
|
|
1025
|
+
function projectArchitectureEdgeStatus(edge) {
|
|
1026
|
+
if (edge.confidence === "low")
|
|
1027
|
+
return "not-proven";
|
|
1028
|
+
if (edge.sourceNode.evidenceStatus === "not-proven" || edge.targetNode.evidenceStatus === "not-proven") {
|
|
1029
|
+
return "not-proven";
|
|
1030
|
+
}
|
|
1031
|
+
if (edge.sourceNode.evidenceStatus === "unknown" || edge.targetNode.evidenceStatus === "unknown") {
|
|
1032
|
+
return "unknown";
|
|
1033
|
+
}
|
|
1034
|
+
return edge.targetNode.evidenceStatus;
|
|
1035
|
+
}
|
|
1036
|
+
function ProjectArchitectureInspector({ focus, node, nodesById, onClose }) {
|
|
1037
|
+
if (!node) {
|
|
1038
|
+
return (_jsxs("aside", { className: "anlyx-map-inspector", "aria-label": "Map inspector", children: [_jsx("button", { className: "anlyx-map-inspector__close", onClick: onClose, type: "button", children: "Close" }), _jsx("h2", { children: "No node selected" })] }));
|
|
1039
|
+
}
|
|
1040
|
+
const upstream = focus.upstreamIds.map((id) => nodesById.get(id)).filter(Boolean);
|
|
1041
|
+
const downstream = focus.downstreamIds.map((id) => nodesById.get(id)).filter(Boolean);
|
|
1042
|
+
const contract = projectArchitectureNodeContract(node);
|
|
1043
|
+
return (_jsxs("aside", { className: "anlyx-map-inspector", "aria-label": "Map inspector", children: [_jsxs("header", { children: [_jsxs("div", { children: [_jsx("span", { children: "Selected" }), _jsx("h2", { children: node.displayLabel ?? node.label })] }), _jsx("button", { className: "anlyx-map-inspector__close", onClick: onClose, type: "button", children: "Close" })] }), _jsxs("dl", { className: "anlyx-map-inspector__meta", children: [_jsxs("div", { children: [_jsx("dt", { children: "Layer" }), _jsx("dd", { children: projectArchitectureKindLabel(node.kind) })] }), _jsxs("div", { children: [_jsx("dt", { children: "Title" }), _jsx("dd", { children: node.label })] }), _jsxs("div", { children: [_jsx("dt", { children: "Path" }), _jsx("dd", { children: formatProjectArchitectureSource(node.source) ?? "Not authored" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Evidence" }), _jsx("dd", { children: node.evidenceLabel })] })] }), _jsx(ProjectArchitectureContractSection, { contract: contract }), _jsxs("section", { children: [_jsx("h3", { children: "Upstream" }), _jsx(ProjectArchitectureInspectorList, { emptyLabel: "No upstream nodes.", nodes: upstream })] }), _jsxs("section", { children: [_jsx("h3", { children: "Downstream path" }), _jsx(ProjectArchitectureInspectorList, { emptyLabel: "No downstream nodes.", nodes: downstream, ordered: true })] })] }));
|
|
1044
|
+
}
|
|
1045
|
+
function ProjectArchitectureContractSection({ contract }) {
|
|
1046
|
+
const hasContract = Boolean(contract.endpoint) ||
|
|
1047
|
+
Boolean(contract.inputName) ||
|
|
1048
|
+
Boolean(contract.outputName) ||
|
|
1049
|
+
contract.relatedModels.length > 0 ||
|
|
1050
|
+
contract.transforms.length > 0 ||
|
|
1051
|
+
Boolean(contract.mapping) ||
|
|
1052
|
+
Boolean(contract.inputShape) ||
|
|
1053
|
+
Boolean(contract.outputShape);
|
|
1054
|
+
return (_jsxs("section", { className: "anlyx-map-contract", children: [_jsx("h3", { children: "Data Contract" }), hasContract ? (_jsxs("div", { className: "anlyx-map-contract__body", children: [contract.endpoint ? (_jsx(ProjectArchitectureContractField, { label: "Endpoint", value: contract.endpoint })) : null, _jsx(ProjectArchitectureContractField, { label: "Input", value: formatProjectArchitectureContractName(contract.inputName, contract.inputKind) }), _jsx(ProjectArchitectureContractField, { label: "Output", value: formatProjectArchitectureContractName(contract.outputName, contract.outputKind) }), _jsx(ProjectArchitectureShapePreview, { shape: contract.outputShape ?? contract.inputShape }), contract.mapping ? (_jsx(ProjectArchitectureContractField, { label: "Mapping", value: contract.mapping })) : null, contract.transforms.length > 0 ? (_jsxs("div", { className: "anlyx-map-contract__group", children: [_jsx("span", { children: "Transforms" }), _jsx("ul", { children: contract.transforms.slice(0, 6).map((transform) => (_jsx("li", { children: transform }, transform))) })] })) : null, contract.relatedModels.length > 0 ? (_jsxs("div", { className: "anlyx-map-contract__group", children: [_jsx("span", { children: "Models" }), _jsx("ul", { children: contract.relatedModels.slice(0, 8).map((model) => (_jsx("li", { children: model }, model))) })] })) : null] })) : (_jsx("p", { className: "project-muted", children: "No contract authored." }))] }));
|
|
1055
|
+
}
|
|
1056
|
+
function ProjectArchitectureContractField({ label, value }) {
|
|
1057
|
+
return (_jsxs("div", { className: "anlyx-map-contract__field", children: [_jsx("span", { children: label }), _jsx("strong", { children: value ?? "No contract authored" })] }));
|
|
1058
|
+
}
|
|
1059
|
+
function ProjectArchitectureShapePreview({ shape }) {
|
|
1060
|
+
if (!shape) {
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
const entries = Object.entries(shape).slice(0, 8);
|
|
1064
|
+
if (entries.length === 0) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
return (_jsxs("div", { className: "anlyx-map-contract__shape", children: [_jsx("span", { children: "Shape" }), _jsxs("pre", { children: [entries.map(([key, value]) => `${key}: ${value}`).join("\n"), Object.keys(shape).length > entries.length ? "\n..." : ""] })] }));
|
|
1068
|
+
}
|
|
1069
|
+
function ProjectArchitectureInspectorList({ emptyLabel, nodes, ordered = false }) {
|
|
1070
|
+
const visibleNodes = nodes.filter((node) => Boolean(node));
|
|
1071
|
+
if (visibleNodes.length === 0) {
|
|
1072
|
+
return _jsx("p", { className: "project-muted", children: emptyLabel });
|
|
1073
|
+
}
|
|
1074
|
+
return (_jsx("ol", { className: ordered ? "anlyx-map-path-list" : "anlyx-map-node-list", children: visibleNodes.slice(0, 8).map((node) => (_jsxs("li", { children: [_jsx("strong", { children: node.displayLabel ?? node.label }), _jsx("span", { children: projectArchitectureKindLabel(node.kind) })] }, node.id))) }));
|
|
1075
|
+
}
|
|
1076
|
+
function projectArchitectureNodeContract(node) {
|
|
1077
|
+
const metadata = recordFromUnknown(node.metadata);
|
|
1078
|
+
const contractRoot = recordFromUnknown(metadata?.["contracts"]) ?? metadata;
|
|
1079
|
+
const input = contractItemFromUnknown(contractRoot?.["input"]);
|
|
1080
|
+
const output = contractItemFromUnknown(contractRoot?.["output"]);
|
|
1081
|
+
return {
|
|
1082
|
+
endpoint: metadataString(contractRoot?.["endpoint"]),
|
|
1083
|
+
inputName: input.name,
|
|
1084
|
+
inputKind: input.kind,
|
|
1085
|
+
inputShape: input.shape,
|
|
1086
|
+
outputName: output.name,
|
|
1087
|
+
outputKind: output.kind,
|
|
1088
|
+
outputShape: output.shape,
|
|
1089
|
+
relatedModels: getStringList(contractRoot?.["relatedModels"]).length > 0
|
|
1090
|
+
? getStringList(contractRoot?.["relatedModels"])
|
|
1091
|
+
: getStringList(contractRoot?.["models"]),
|
|
1092
|
+
transforms: getStringList(contractRoot?.["transforms"]),
|
|
1093
|
+
mapping: metadataString(contractRoot?.["mapping"])
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function contractItemFromUnknown(value) {
|
|
1097
|
+
if (typeof value === "string") {
|
|
1098
|
+
return { name: value, kind: undefined, shape: undefined };
|
|
1099
|
+
}
|
|
1100
|
+
const record = recordFromUnknown(value);
|
|
1101
|
+
if (!record) {
|
|
1102
|
+
return { name: undefined, kind: undefined, shape: undefined };
|
|
1103
|
+
}
|
|
1104
|
+
return {
|
|
1105
|
+
name: metadataString(record["name"]),
|
|
1106
|
+
kind: metadataString(record["kind"]),
|
|
1107
|
+
shape: shapeFromUnknown(record["shape"])
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function shapeFromUnknown(value) {
|
|
1111
|
+
const record = recordFromUnknown(value);
|
|
1112
|
+
if (!record) {
|
|
1113
|
+
return undefined;
|
|
1114
|
+
}
|
|
1115
|
+
const entries = Object.entries(record)
|
|
1116
|
+
.map(([key, item]) => [key, typeof item === "string" ? item : JSON.stringify(item)])
|
|
1117
|
+
.filter((entry) => typeof entry[1] === "string");
|
|
1118
|
+
return entries.length > 0 ? Object.fromEntries(entries) : undefined;
|
|
1119
|
+
}
|
|
1120
|
+
function recordFromUnknown(value) {
|
|
1121
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1122
|
+
return undefined;
|
|
1123
|
+
}
|
|
1124
|
+
return value;
|
|
1125
|
+
}
|
|
1126
|
+
function formatProjectArchitectureContractName(name, kind) {
|
|
1127
|
+
if (!name) {
|
|
1128
|
+
return undefined;
|
|
1129
|
+
}
|
|
1130
|
+
return kind ? `${name} (${kind})` : name;
|
|
1131
|
+
}
|
|
1132
|
+
function projectArchitectureEvidenceStatus(evidenceIds, evidenceById) {
|
|
1133
|
+
const statuses = evidenceIds
|
|
1134
|
+
.map((id) => evidenceById.get(id)?.status)
|
|
1135
|
+
.filter((status) => Boolean(status));
|
|
1136
|
+
if (statuses.includes("source-matched"))
|
|
1137
|
+
return "source-matched";
|
|
1138
|
+
if (statuses.includes("observed"))
|
|
1139
|
+
return "observed";
|
|
1140
|
+
if (statuses.includes("measured"))
|
|
1141
|
+
return "measured";
|
|
1142
|
+
if (statuses.includes("agent-inferred"))
|
|
1143
|
+
return "agent-inferred";
|
|
1144
|
+
if (statuses.includes("not-proven"))
|
|
1145
|
+
return "not-proven";
|
|
1146
|
+
return "unknown";
|
|
1147
|
+
}
|
|
1148
|
+
function projectArchitectureNodeSubtitle(node) {
|
|
1149
|
+
const module = metadataString(node.metadata?.["module"]);
|
|
1150
|
+
const path = metadataString(node.metadata?.["path"]);
|
|
1151
|
+
const detail = metadataString(node.metadata?.["detail"]);
|
|
1152
|
+
return module ?? path ?? detail ?? node.domain ?? node.id;
|
|
1153
|
+
}
|
|
1154
|
+
function formatProjectArchitectureSource(source) {
|
|
1155
|
+
if (!source)
|
|
1156
|
+
return undefined;
|
|
1157
|
+
const line = source.lineStart ? `:${source.lineStart}` : "";
|
|
1158
|
+
const symbol = source.symbol ? ` ${source.symbol}` : "";
|
|
1159
|
+
return `${source.filePath}${line}${symbol}`;
|
|
1160
|
+
}
|
|
1161
|
+
function projectArchitectureKindLabel(kind) {
|
|
1162
|
+
const labels = {
|
|
1163
|
+
frontend: "Page",
|
|
1164
|
+
request: "HTTP",
|
|
1165
|
+
api: "API",
|
|
1166
|
+
controller: "CTRL",
|
|
1167
|
+
handler: "Handler",
|
|
1168
|
+
middleware: "Middleware",
|
|
1169
|
+
service: "SRV",
|
|
1170
|
+
policy: "Policy",
|
|
1171
|
+
mapper: "Map",
|
|
1172
|
+
repository: "Repo",
|
|
1173
|
+
database: "Data",
|
|
1174
|
+
cache: "Cache",
|
|
1175
|
+
queue: "Queue",
|
|
1176
|
+
job: "Job",
|
|
1177
|
+
external: "External",
|
|
1178
|
+
result: "UI",
|
|
1179
|
+
unknown: "Unknown"
|
|
1180
|
+
};
|
|
1181
|
+
return labels[kind] ?? titleCase(kind);
|
|
1182
|
+
}
|
|
1183
|
+
function projectArchitectureNodeIcon(kind) {
|
|
1184
|
+
if (kind === "database") {
|
|
1185
|
+
return Database;
|
|
1186
|
+
}
|
|
1187
|
+
if (kind === "repository") {
|
|
1188
|
+
return Box;
|
|
1189
|
+
}
|
|
1190
|
+
if (kind === "service" || kind === "policy" || kind === "mapper") {
|
|
1191
|
+
return Layers3;
|
|
1192
|
+
}
|
|
1193
|
+
if (kind === "controller" || kind === "handler" || kind === "middleware") {
|
|
1194
|
+
return FileCode2;
|
|
1195
|
+
}
|
|
1196
|
+
if (kind === "api" || kind === "request") {
|
|
1197
|
+
return Network;
|
|
1198
|
+
}
|
|
1199
|
+
if (kind === "result") {
|
|
1200
|
+
return Flag;
|
|
1201
|
+
}
|
|
1202
|
+
return Circle;
|
|
1203
|
+
}
|
|
1204
|
+
function ProjectJsonView({ data, model, rawJson, validationReport }) {
|
|
1205
|
+
const jsonFiles = useMemo(() => projectJsonFiles(data, model, rawJson, validationReport), [data, model, rawJson, validationReport]);
|
|
1206
|
+
const [selectedJsonFileId, setSelectedJsonFileId] = useState(jsonFiles[0]?.id ?? "project");
|
|
1207
|
+
const selectedJsonFile = jsonFiles.find((file) => file.id === selectedJsonFileId) ?? jsonFiles[0];
|
|
1208
|
+
const activeJson = selectedJsonFile?.content ?? rawJson;
|
|
1209
|
+
const lines = activeJson.split("\n");
|
|
1210
|
+
const inventory = useMemo(() => projectJsonInventoryItems(data, model), [data, model]);
|
|
1211
|
+
const EditorFileIcon = selectedJsonFile?.icon ?? Code2;
|
|
1212
|
+
const validationTone = validationReport
|
|
1213
|
+
? validationReport.valid
|
|
1214
|
+
? validationReport.issues.length > 0
|
|
1215
|
+
? "amber"
|
|
1216
|
+
: "green"
|
|
1217
|
+
: "red"
|
|
1218
|
+
: "green";
|
|
1219
|
+
const validationLabel = validationReport
|
|
1220
|
+
? validationReport.valid
|
|
1221
|
+
? validationReport.issues.length > 0
|
|
1222
|
+
? "Valid with warnings"
|
|
1223
|
+
: "Valid"
|
|
1224
|
+
: "Invalid"
|
|
1225
|
+
: "Valid";
|
|
1226
|
+
return (_jsxs("section", { className: "project-json-workspace", "aria-label": "Project JSON", children: [_jsxs("aside", { className: "project-json-outline", "aria-label": "JSON files", children: [_jsx("div", { className: "project-panel-title", children: _jsx("h2", { children: "JSON Files" }) }), _jsx("span", { className: "project-badge project-badge--green", children: "AI-authored" }), _jsx("div", { className: "project-json-file-list", role: "list", children: jsonFiles.map((file) => {
|
|
1227
|
+
const Icon = file.icon;
|
|
1228
|
+
return (_jsxs("button", { className: file.id === selectedJsonFile?.id ? "is-selected" : "", type: "button", onClick: () => setSelectedJsonFileId(file.id), children: [_jsx(Icon, { size: 17 }), _jsxs("span", { children: [_jsx("strong", { children: file.name }), _jsx("small", { children: file.description })] }), _jsx("em", { children: file.countLabel })] }, file.id));
|
|
1229
|
+
}) })] }), _jsxs("article", { className: "project-json-editor", "aria-label": "Raw project JSON", children: [_jsxs("header", { children: [_jsxs("span", { className: "project-json-file", children: [_jsx(EditorFileIcon, { size: 16 }), selectedJsonFile?.name ?? "anlyx.project.json"] }), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => void navigator.clipboard?.writeText(activeJson), children: [_jsx(Copy, { size: 15 }), "Copy JSON"] }), _jsx("button", { type: "button", onClick: () => downloadProjectJson(activeJson, selectedJsonFile?.name), children: _jsx(Download, { size: 15 }) })] })] }), _jsx("pre", { children: lines.map((line, index) => (_jsxs("span", { children: [_jsx("em", { children: index + 1 }), _jsx("code", { children: line || " " })] }, `${index}:${line}`))) })] }), _jsxs("aside", { className: "project-json-details", "aria-label": "JSON details", children: [_jsxs(JsonDetailsCard, { title: "Schema", children: [_jsx(ProjectDetailRow, { label: "Version", value: data.schemaVersion }), _jsx(ProjectDetailRow, { label: "Validation", value: validationLabel, tone: validationTone })] }), validationReport ? (_jsxs(JsonDetailsCard, { title: "Trust checks", children: [_jsx(ProjectDetailRow, { label: "Source issues", value: String(validationReport.summary.sourceIssueCount), tone: validationReport.summary.sourceIssueCount > 0 ? "amber" : "green" }), validationReport.summary.sourceIssueBreakdown ? (_jsxs(_Fragment, { children: [_jsx(ProjectDetailRow, { label: "Missing files", value: String(validationReport.summary.sourceIssueBreakdown.missingFiles), tone: validationReport.summary.sourceIssueBreakdown.missingFiles > 0
|
|
1230
|
+
? "amber"
|
|
1231
|
+
: "green" }), _jsx(ProjectDetailRow, { label: "Line issues", value: String(validationReport.summary.sourceIssueBreakdown.placeholderLines +
|
|
1232
|
+
validationReport.summary.sourceIssueBreakdown.outOfRangeLines), tone: validationReport.summary.sourceIssueBreakdown.placeholderLines +
|
|
1233
|
+
validationReport.summary.sourceIssueBreakdown.outOfRangeLines >
|
|
1234
|
+
0
|
|
1235
|
+
? "amber"
|
|
1236
|
+
: "green" }), _jsx(ProjectDetailRow, { label: "Symbol issues", value: String(validationReport.summary.sourceIssueBreakdown.missingSymbols), tone: validationReport.summary.sourceIssueBreakdown.missingSymbols > 0
|
|
1237
|
+
? "amber"
|
|
1238
|
+
: "green" })] })) : null, _jsx(ProjectDetailRow, { label: "Coverage", value: validationReport.summary.coverageStatus, tone: validationReport.summary.coverageStatus === "partial" ? "amber" : "green" }), _jsx(ProjectDetailRow, { label: "Issues", value: String(validationReport.issues.length), tone: validationReport.issues.length > 0 ? "amber" : "green" })] })) : null, data.coverage || validationReport ? (_jsxs(JsonDetailsCard, { title: "Coverage", children: [_jsx(ProjectDetailRow, { label: "Pages", value: coverageValue(validationReport?.summary.modeled.pages ?? data.pages.length, validationReport?.summary.detected?.pages ?? data.coverage?.detected?.pages) }), _jsx(ProjectDetailRow, { label: "Requests", value: coverageValue(validationReport?.summary.modeled.requests ?? data.requests.length, data.coverage?.detected?.requests ?? data.coverage?.detected?.backendEndpoints) }), _jsx(ProjectDetailRow, { label: "Flows", value: coverageValue(validationReport?.summary.modeled.flows ?? data.flows.length, validationReport?.summary.detected?.flows ?? data.coverage?.detected?.flows) })] })) : null, _jsxs(JsonDetailsCard, { title: "Project", children: [_jsx(ProjectDetailRow, { label: "Name", value: data.project.name }), _jsx(ProjectDetailRow, { label: "ID", value: data.project.id }), _jsx(ProjectDetailRow, { label: "Root", value: data.project.root ?? "not provided" })] }), _jsx(JsonDetailsCard, { title: "Counts", children: _jsx("div", { className: "project-json-counts", children: inventory.map((item) => (_jsx(JsonInventoryItem, { item: item }, item.id))) }) })] })] }));
|
|
1239
|
+
}
|
|
1240
|
+
function JsonInventoryItem({ item }) {
|
|
1241
|
+
const Icon = item.icon;
|
|
1242
|
+
return (_jsxs("a", { className: "project-json-count-item", href: `#json-${item.id}`, children: [_jsx(Icon, { size: 16 }), _jsx("span", { children: item.label }), _jsx("strong", { children: item.value })] }));
|
|
1243
|
+
}
|
|
1244
|
+
function projectJsonFiles(data, model, rawJson, validationReport) {
|
|
1245
|
+
const files = [
|
|
1246
|
+
{
|
|
1247
|
+
id: "project",
|
|
1248
|
+
name: projectSourceFile(model),
|
|
1249
|
+
description: "Complete project document",
|
|
1250
|
+
countLabel: "full",
|
|
1251
|
+
content: rawJson,
|
|
1252
|
+
icon: Code2
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
id: "index",
|
|
1256
|
+
name: ".anlyx/project/index.json",
|
|
1257
|
+
description: "Schema and project metadata",
|
|
1258
|
+
countLabel: data.schemaVersion,
|
|
1259
|
+
content: projectJsonString({ schemaVersion: data.schemaVersion, project: data.project }),
|
|
1260
|
+
icon: Folder
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
id: "areas",
|
|
1264
|
+
name: ".anlyx/project/areas.json",
|
|
1265
|
+
description: "Project areas",
|
|
1266
|
+
countLabel: String(model.totals.areas),
|
|
1267
|
+
content: projectJsonString(data.areas),
|
|
1268
|
+
icon: BriefcaseBusiness
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
id: "pages",
|
|
1272
|
+
name: ".anlyx/project/pages.json",
|
|
1273
|
+
description: "Page index and routes",
|
|
1274
|
+
countLabel: String(model.totals.pages),
|
|
1275
|
+
content: projectJsonString(data.pages),
|
|
1276
|
+
icon: BookOpen
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
id: "features",
|
|
1280
|
+
name: ".anlyx/project/features.json",
|
|
1281
|
+
description: "Page capabilities",
|
|
1282
|
+
countLabel: String(model.totals.features),
|
|
1283
|
+
content: projectJsonString(data.features),
|
|
1284
|
+
icon: FileText
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
id: "overview",
|
|
1288
|
+
name: ".anlyx/project/overview.json",
|
|
1289
|
+
description: "Human project summary",
|
|
1290
|
+
countLabel: data.overview.summary ? "authored" : "empty",
|
|
1291
|
+
content: projectJsonString(data.overview),
|
|
1292
|
+
icon: BookOpen
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
id: "capabilities",
|
|
1296
|
+
name: ".anlyx/project/capabilities.json",
|
|
1297
|
+
description: "Readable product capabilities",
|
|
1298
|
+
countLabel: String(data.capabilities.length),
|
|
1299
|
+
content: projectJsonString(data.capabilities),
|
|
1300
|
+
icon: Workflow
|
|
1301
|
+
},
|
|
1302
|
+
{
|
|
1303
|
+
id: "requests",
|
|
1304
|
+
name: ".anlyx/project/requests.json",
|
|
1305
|
+
description: "Frontend and API requests",
|
|
1306
|
+
countLabel: String(model.totals.requests),
|
|
1307
|
+
content: projectJsonString(data.requests),
|
|
1308
|
+
icon: Network
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
id: "flows",
|
|
1312
|
+
name: ".anlyx/project/flows.json",
|
|
1313
|
+
description: "Request-to-backend paths",
|
|
1314
|
+
countLabel: String(model.totals.flows),
|
|
1315
|
+
content: projectJsonString(data.flows),
|
|
1316
|
+
icon: Workflow
|
|
1317
|
+
},
|
|
1318
|
+
{
|
|
1319
|
+
id: "architecture",
|
|
1320
|
+
name: ".anlyx/project/architecture.json",
|
|
1321
|
+
description: "Architecture nodes and edges",
|
|
1322
|
+
countLabel: String(model.totals.architectureNodes),
|
|
1323
|
+
content: projectJsonString(data.architecture),
|
|
1324
|
+
icon: Layers3
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
id: "evidence",
|
|
1328
|
+
name: ".anlyx/project/evidence.json",
|
|
1329
|
+
description: "Proof and confidence sources",
|
|
1330
|
+
countLabel: String(model.totals.evidence),
|
|
1331
|
+
content: projectJsonString(data.evidence),
|
|
1332
|
+
icon: ShieldCheck
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
id: "data-lifecycles",
|
|
1336
|
+
name: ".anlyx/project/data-lifecycles.json",
|
|
1337
|
+
description: "Core data lifecycle maps",
|
|
1338
|
+
countLabel: String(data.dataLifecycles.length),
|
|
1339
|
+
content: projectJsonString(data.dataLifecycles),
|
|
1340
|
+
icon: Database
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
id: "impact-maps",
|
|
1344
|
+
name: ".anlyx/project/impact-maps.json",
|
|
1345
|
+
description: "Product impact maps",
|
|
1346
|
+
countLabel: String(data.impactMaps.length),
|
|
1347
|
+
content: projectJsonString(data.impactMaps),
|
|
1348
|
+
icon: Network
|
|
1349
|
+
},
|
|
1350
|
+
{
|
|
1351
|
+
id: "coverage",
|
|
1352
|
+
name: ".anlyx/project/coverage.json",
|
|
1353
|
+
description: "Detected and modeled coverage",
|
|
1354
|
+
countLabel: data.coverage?.status ?? "unknown",
|
|
1355
|
+
content: projectJsonString(data.coverage ?? { status: "unknown" }),
|
|
1356
|
+
icon: Gauge
|
|
1357
|
+
},
|
|
1358
|
+
{
|
|
1359
|
+
id: "dictionary",
|
|
1360
|
+
name: ".anlyx/project/dictionary.json",
|
|
1361
|
+
description: "Language and term dictionary",
|
|
1362
|
+
countLabel: String(data.dictionary.terms.length),
|
|
1363
|
+
content: projectJsonString(data.dictionary),
|
|
1364
|
+
icon: Languages
|
|
1365
|
+
}
|
|
1366
|
+
];
|
|
1367
|
+
if (data.measurements.length > 0) {
|
|
1368
|
+
files.push({
|
|
1369
|
+
id: "measurements",
|
|
1370
|
+
name: ".anlyx/project/measurements.json",
|
|
1371
|
+
description: "Optional runtime measurements",
|
|
1372
|
+
countLabel: String(model.totals.measurements),
|
|
1373
|
+
content: projectJsonString(data.measurements),
|
|
1374
|
+
icon: Clock3
|
|
1375
|
+
});
|
|
1376
|
+
}
|
|
1377
|
+
if (validationReport) {
|
|
1378
|
+
files.push({
|
|
1379
|
+
id: "validation-report",
|
|
1380
|
+
name: ".anlyx/validation-report.json",
|
|
1381
|
+
description: "Source and coverage validation",
|
|
1382
|
+
countLabel: `${validationReport.issues.length} issues`,
|
|
1383
|
+
content: projectJsonString(validationReport),
|
|
1384
|
+
icon: ShieldCheck
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
return files;
|
|
1388
|
+
}
|
|
1389
|
+
function coverageValue(modeled, detected) {
|
|
1390
|
+
return detected === undefined ? String(modeled) : `${modeled} / ${detected}`;
|
|
1391
|
+
}
|
|
1392
|
+
function formatSourceIssueDetails(breakdown) {
|
|
1393
|
+
if (!breakdown) {
|
|
1394
|
+
return "";
|
|
1395
|
+
}
|
|
1396
|
+
const lineIssues = breakdown.placeholderLines + breakdown.outOfRangeLines;
|
|
1397
|
+
const parts = [
|
|
1398
|
+
breakdown.missingFiles > 0 ? `${breakdown.missingFiles} missing file` : "",
|
|
1399
|
+
breakdown.missingSymbols > 0 ? `${breakdown.missingSymbols} symbol` : "",
|
|
1400
|
+
lineIssues > 0 ? `${lineIssues} line` : ""
|
|
1401
|
+
].filter(Boolean);
|
|
1402
|
+
return parts.join(" / ");
|
|
1403
|
+
}
|
|
1404
|
+
function projectJsonInventoryItems(data, model) {
|
|
1405
|
+
return [
|
|
1406
|
+
{ id: "schemaVersion", label: "schemaVersion", value: data.schemaVersion, icon: Braces },
|
|
1407
|
+
{ id: "project", label: "project", value: data.project.name, icon: Folder },
|
|
1408
|
+
{ id: "areas", label: "areas", value: String(model.totals.areas), icon: BriefcaseBusiness },
|
|
1409
|
+
{ id: "pages", label: "pages", value: String(model.totals.pages), icon: BookOpen },
|
|
1410
|
+
{ id: "features", label: "features", value: String(model.totals.features), icon: FileText },
|
|
1411
|
+
{
|
|
1412
|
+
id: "overview",
|
|
1413
|
+
label: "overview",
|
|
1414
|
+
value: data.overview.summary ? "authored" : "empty",
|
|
1415
|
+
icon: BookOpen
|
|
1416
|
+
},
|
|
1417
|
+
{
|
|
1418
|
+
id: "capabilities",
|
|
1419
|
+
label: "capabilities",
|
|
1420
|
+
value: String(data.capabilities.length),
|
|
1421
|
+
icon: Workflow
|
|
1422
|
+
},
|
|
1423
|
+
{ id: "requests", label: "requests", value: String(model.totals.requests), icon: Network },
|
|
1424
|
+
{ id: "flows", label: "flows", value: String(model.totals.flows), icon: Workflow },
|
|
1425
|
+
{
|
|
1426
|
+
id: "architecture",
|
|
1427
|
+
label: "architecture",
|
|
1428
|
+
value: String(model.totals.architectureNodes),
|
|
1429
|
+
icon: Layers3
|
|
1430
|
+
},
|
|
1431
|
+
{ id: "evidence", label: "evidence", value: String(model.totals.evidence), icon: ShieldCheck },
|
|
1432
|
+
{
|
|
1433
|
+
id: "dataLifecycles",
|
|
1434
|
+
label: "dataLifecycles",
|
|
1435
|
+
value: String(data.dataLifecycles.length),
|
|
1436
|
+
icon: Database
|
|
1437
|
+
},
|
|
1438
|
+
{
|
|
1439
|
+
id: "impactMaps",
|
|
1440
|
+
label: "impactMaps",
|
|
1441
|
+
value: String(data.impactMaps.length),
|
|
1442
|
+
icon: Network
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
id: "measurements",
|
|
1446
|
+
label: "measurements",
|
|
1447
|
+
value: String(model.totals.measurements),
|
|
1448
|
+
icon: Clock3
|
|
1449
|
+
},
|
|
1450
|
+
{
|
|
1451
|
+
id: "dictionary",
|
|
1452
|
+
label: "dictionary",
|
|
1453
|
+
value: String(data.dictionary.terms.length),
|
|
1454
|
+
icon: Languages
|
|
1455
|
+
}
|
|
1456
|
+
];
|
|
1457
|
+
}
|
|
1458
|
+
function projectJsonString(value) {
|
|
1459
|
+
return JSON.stringify(value, null, 2);
|
|
1460
|
+
}
|
|
1461
|
+
function JsonDetailsCard({ children, title }) {
|
|
1462
|
+
return (_jsxs("section", { className: "project-json-details-card", children: [_jsx("h2", { children: title }), children] }));
|
|
1463
|
+
}
|
|
1464
|
+
function ProjectDetailRow({ label, tone, value }) {
|
|
1465
|
+
return (_jsxs("div", { className: "project-detail-row", children: [_jsx("dt", { children: label }), _jsx("dd", { className: tone ? `is-${tone}` : "", children: value })] }));
|
|
1466
|
+
}
|
|
1467
|
+
function ProjectStatusBar({ data, locale, model, validationReport }) {
|
|
1468
|
+
const sourceFile = projectSourceFile(model);
|
|
1469
|
+
const generatedBy = projectAgentName(model);
|
|
1470
|
+
const pageCoverage = projectPageCoverageSummary(data, validationReport);
|
|
1471
|
+
const coverageStatus = validationReport?.summary.coverageStatus ?? data.coverage?.status;
|
|
1472
|
+
const sourceIssueCount = validationReport?.summary.sourceIssueCount ?? 0;
|
|
1473
|
+
const hasTrustWarning = coverageStatus === "partial" || sourceIssueCount > 0;
|
|
1474
|
+
return (_jsxs("div", { className: "project-statusbar", "aria-label": "Project source status", children: [_jsxs("div", { className: "project-statusbar__provenance", children: [_jsxs("span", { children: [_jsx(Code2, { size: 15 }), tp(locale, "source"), ": ", _jsx("strong", { children: sourceFile })] }), _jsxs("span", { children: [_jsx(Zap, { size: 15 }), tp(locale, "agent"), ": ", _jsx("strong", { children: generatedBy })] })] }), _jsxs("div", { className: "project-statusbar__summary", children: [_jsx("strong", { children: pageCoverage.label }), coverageStatus ? (_jsxs("span", { className: `project-status ${hasTrustWarning ? "project-status--amber" : "project-status--green"}`, children: [_jsx(ShieldCheck, { size: 15 }), coverageStatus === "partial" ? "Partial analysis" : "Coverage checked"] })) : null, _jsxs("span", { children: [_jsx(Clock3, { size: 15 }), tp(locale, "lastAnalysis"), ": ", formatDateTime(model.project.analyzedAt)] }), _jsxs("span", { className: "project-status project-status--green", children: [_jsx(Check, { size: 15 }), tp(locale, "upToDate")] })] })] }));
|
|
1475
|
+
}
|
|
1476
|
+
function projectPageCoverageSummary(data, validationReport) {
|
|
1477
|
+
const modeled = validationReport?.summary.modeled.pages ?? data.coverage?.modeled?.pages ?? data.pages.length;
|
|
1478
|
+
const detected = validationReport?.summary.detected?.pages ?? data.coverage?.detected?.pages;
|
|
1479
|
+
const value = coverageValue(modeled, detected);
|
|
1480
|
+
return {
|
|
1481
|
+
detected,
|
|
1482
|
+
label: detected !== undefined ? `${value} pages modeled` : `${modeled} pages analyzed`,
|
|
1483
|
+
modeled,
|
|
1484
|
+
value
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function evidenceStatusLabel(status) {
|
|
1488
|
+
if (status === "source-matched")
|
|
1489
|
+
return "Source-matched";
|
|
1490
|
+
if (status === "agent-inferred")
|
|
1491
|
+
return "Agent-inferred";
|
|
1492
|
+
if (status === "not-proven")
|
|
1493
|
+
return "Not-proven";
|
|
1494
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
1495
|
+
}
|
|
1496
|
+
function downloadProjectJson(rawJson, fileName = "anlyx.project.json") {
|
|
1497
|
+
const blob = new Blob([rawJson], { type: "application/json" });
|
|
1498
|
+
const url = URL.createObjectURL(blob);
|
|
1499
|
+
const link = document.createElement("a");
|
|
1500
|
+
const safeFileName = fileName.split("/").pop() ?? "anlyx.project.json";
|
|
1501
|
+
link.href = url;
|
|
1502
|
+
link.download = safeFileName;
|
|
1503
|
+
link.click();
|
|
1504
|
+
URL.revokeObjectURL(url);
|
|
1505
|
+
}
|
|
1506
|
+
function WorkspaceSidebar({ activeView, locale, onLocaleChange, onViewChange }) {
|
|
1507
|
+
return (_jsxs("aside", { className: "workspace-sidebar workspace-sidebar--timing", "aria-label": t(locale, "sidebarLabel"), children: [_jsx("div", { className: "workspace-brand workspace-brand--wordmark", children: _jsx("img", { alt: "Anlyx", src: "/workspace/anlyx-logo-transparent.png" }) }), _jsxs("nav", { className: "workspace-nav", "aria-label": t(locale, "primaryNavLabel"), children: [_jsxs("button", { className: activeView === "flows" ? "is-active" : "", type: "button", onClick: () => onViewChange("flows"), children: [_jsx(Workflow, { size: 19 }), _jsx("span", { children: t(locale, "flows") })] }), _jsxs("button", { className: activeView === "map" ? "is-active" : "", type: "button", onClick: () => onViewChange("map"), children: [_jsx(Network, { size: 19 }), _jsx("span", { children: "Map" })] }), _jsxs("button", { className: activeView === "json" ? "is-active" : "", type: "button", onClick: () => onViewChange("json"), children: [_jsx(FileCode2, { size: 19 }), _jsx("span", { children: "JSON" })] })] }), _jsx("div", { className: "workspace-sidebar__bottom", children: _jsxs("div", { className: "language-toggle", "aria-label": t(locale, "language"), children: [_jsx("button", { className: locale === "en" ? "is-active" : "", type: "button", onClick: () => onLocaleChange("en"), children: "EN" }), _jsx("button", { className: locale === "ko" ? "is-active" : "", type: "button", onClick: () => onLocaleChange("ko"), children: "\uD55C" })] }) })] }));
|
|
1508
|
+
}
|
|
1509
|
+
function WorkspaceHeader({ activeTab, data, onTabChange, record, recordCount }) {
|
|
1510
|
+
const locale = useWorkspaceLocale();
|
|
1511
|
+
return (_jsxs("header", { className: "workspace-header", children: [_jsxs("div", { className: "workspace-header__top", children: [_jsxs("div", { children: [_jsxs("div", { className: "breadcrumbs", children: [_jsx("span", { children: t(locale, "flows") }), _jsx(ChevronRight, { size: 13 }), _jsxs("strong", { children: [t(locale, "flow"), " ", record ? compactId(record.requestId) : t(locale, "waiting")] })] }), _jsxs("div", { className: "title-row", children: [_jsx("h1", { children: record ? t(locale, "matchedBackendFlow") : t(locale, "liveWorkspace") }), _jsx("span", { className: `capture-dot ${recordCount > 0 ? "is-live" : ""}`, "aria-label": recordCount > 0 ? t(locale, "captureConnected") : t(locale, "waitingForCapture") })] }), _jsx("p", { children: record
|
|
1512
|
+
? t(locale, "matchedSubtitle")
|
|
1513
|
+
: `${data.projectName} ${t(locale, "readyPrefix")}${t(locale, "readySuffix")}` })] }), _jsxs("div", { className: "header-meta", children: [_jsxs("span", { children: [_jsx(Clock3, { size: 15 }), t(locale, "lastScan"), " ", formatDateTime(data.generatedAt)] }), _jsxs("span", { children: [_jsx(Clock3, { size: 15 }), formatDateTime(record?.createdAt)] })] })] }), _jsx("div", { className: "workspace-header__tabs", children: _jsx(FlowTabs, { activeTab: activeTab, onTabChange: onTabChange }) })] }));
|
|
1514
|
+
}
|
|
1515
|
+
function FlowTabs({ activeTab, onTabChange }) {
|
|
1516
|
+
const locale = useWorkspaceLocale();
|
|
1517
|
+
return (_jsx("div", { className: "flow-tabs", role: "tablist", "aria-label": t(locale, "flowViews"), children: ["summary", "timing", "diagram"].map((tab) => (_jsx("button", { "aria-selected": activeTab === tab, className: activeTab === tab ? "is-active" : "", role: "tab", type: "button", onClick: () => onTabChange(tab), children: tabLabel(tab, locale) }, tab))) }));
|
|
1518
|
+
}
|
|
1519
|
+
function JsonReaderView({ data }) {
|
|
1520
|
+
const [activeTab, setActiveTab] = useState("overview");
|
|
1521
|
+
const locale = useWorkspaceLocale();
|
|
1522
|
+
const flows = data.flows;
|
|
1523
|
+
const endpointsById = useMemo(() => new Map(data.endpoints.map((endpoint) => [endpoint.id, endpoint])), [data.endpoints]);
|
|
1524
|
+
const rawJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
|
1525
|
+
const stats = useMemo(() => jsonStats(data), [data]);
|
|
1526
|
+
return (_jsxs("div", { className: "json-reader-layout", id: "json", children: [_jsxs("header", { className: "json-reader-hero", children: [_jsxs("div", { children: [_jsxs("div", { className: "breadcrumbs", children: [_jsx("span", { children: t(locale, "flows") }), _jsx(ChevronRight, { size: 13 }), _jsx("strong", { children: "JSON" })] }), _jsxs("div", { className: "title-row", children: [_jsx("h1", { children: "JSON workspace" }), _jsx("span", { className: "capture-dot is-live", "aria-label": "JSON loaded" })] }), _jsx("p", { children: "Inspect the imported snapshot exactly as Anlyx reads it: schema, flows, evidence, warnings, and raw data." })] }), _jsxs("div", { className: "json-reader-source", children: [_jsx("span", { children: "loaded from" }), _jsx("strong", { children: "report-data.json" }), _jsx("small", { children: formatDateTime(data.generatedAt) })] })] }), _jsxs("section", { className: "json-stat-grid", "aria-label": "JSON inventory", children: [_jsx(JsonStatCard, { label: "schema", value: data.schemaVersion, detail: data.projectName }), _jsx(JsonStatCard, { label: "requests", value: String(stats.endpointCount), detail: `${stats.flowCount} mapped flows` }), _jsx(JsonStatCard, { label: "graph", value: `${stats.nodeCount} / ${stats.edgeCount}`, detail: "nodes / edges" }), _jsx(JsonStatCard, { label: "evidence", value: String(stats.evidenceCount), detail: `${stats.warningCount} warnings` })] }), _jsxs("section", { className: "json-reader-panel", children: [_jsx("div", { className: "json-reader-tabs", role: "tablist", "aria-label": "JSON reader views", children: ["overview", "flows", "raw"].map((tab) => (_jsx("button", { "aria-selected": activeTab === tab, className: activeTab === tab ? "is-active" : "", role: "tab", type: "button", onClick: () => setActiveTab(tab), children: jsonTabLabel(tab) }, tab))) }), activeTab === "overview" ? (_jsx(JsonOverview, { data: data, stats: stats })) : activeTab === "flows" ? (_jsx("div", { className: "json-flow-list", children: flows.map((flow) => (_jsx(JsonFlowCard, { endpoint: endpointsById.get(flow.endpointId), flow: flow }, flow.endpointId))) })) : (_jsxs("div", { className: "json-raw-view", children: [_jsxs("div", { className: "json-raw-view__header", children: [_jsxs("div", { children: [_jsx("strong", { children: "Raw imported JSON" }), _jsx("span", { children: "Pretty printed, read-only" })] }), _jsx("code", { children: formatBytes(rawJson.length) })] }), _jsx("pre", { children: rawJson })] }))] })] }));
|
|
1527
|
+
}
|
|
1528
|
+
function JsonStatCard({ detail, label, value }) {
|
|
1529
|
+
return (_jsxs("article", { className: "json-stat-card", children: [_jsx("span", { children: label }), _jsx("strong", { title: value, children: value }), _jsx("small", { children: detail })] }));
|
|
1530
|
+
}
|
|
1531
|
+
function JsonOverview({ data, stats }) {
|
|
1532
|
+
const topMethods = methodCounts(data);
|
|
1533
|
+
const topWarnings = data.warnings.slice(0, 4);
|
|
1534
|
+
return (_jsxs("div", { className: "json-overview-grid", children: [_jsxs("article", { className: "json-section-card", children: [_jsx("h2", { children: "Document" }), _jsxs("dl", { className: "json-kv-list", children: [_jsxs("div", { children: [_jsx("dt", { children: "projectName" }), _jsx("dd", { children: data.projectName })] }), _jsxs("div", { children: [_jsx("dt", { children: "schemaVersion" }), _jsx("dd", { children: data.schemaVersion })] }), _jsxs("div", { children: [_jsx("dt", { children: "generatedAt" }), _jsx("dd", { children: formatDateTime(data.generatedAt) })] }), _jsxs("div", { children: [_jsx("dt", { children: "pages" }), _jsx("dd", { children: stats.pageCount })] }), _jsxs("div", { children: [_jsx("dt", { children: "edges" }), _jsx("dd", { children: stats.edgeCount })] }), _jsxs("div", { children: [_jsx("dt", { children: "capture" }), _jsx("dd", { children: data.capture ? "included" : "not included" })] })] })] }), _jsxs("article", { className: "json-section-card", children: [_jsx("h2", { children: "Requests" }), _jsx("div", { className: "json-method-bars", children: topMethods.map(([method, count]) => (_jsxs("div", { children: [_jsx("span", { children: method }), _jsx("i", { style: {
|
|
1535
|
+
width: `${Math.max(12, (count / Math.max(stats.endpointCount, 1)) * 100)}%`
|
|
1536
|
+
} }), _jsx("strong", { children: count })] }, method))) })] }), _jsxs("article", { className: "json-section-card json-section-card--wide", children: [_jsx("h2", { children: "Flow index" }), _jsx("div", { className: "json-path-preview", children: data.flows.slice(0, 5).map((flow) => {
|
|
1537
|
+
const endpoint = data.endpoints.find((item) => item.id === flow.endpointId);
|
|
1538
|
+
return (_jsxs("div", { children: [_jsx("span", { children: endpoint ? `${endpoint.method} ${endpoint.path}` : flow.endpointId }), _jsxs("strong", { children: [flow.nodes.length, " nodes \u00B7 ", flow.edges.length, " edges"] })] }, flow.endpointId));
|
|
1539
|
+
}) })] }), _jsxs("article", { className: "json-section-card json-section-card--wide", children: [_jsx("h2", { children: "Warnings" }), _jsx("div", { className: "json-warning-list", children: topWarnings.length > 0 ? (topWarnings.map((warning, index) => (_jsxs("div", { children: [_jsx("code", { children: warning.code }), _jsx("span", { children: warning.message })] }, `${warning.code}-${index}`)))) : (_jsx("p", { children: "No warnings in the imported report data." })) })] })] }));
|
|
1540
|
+
}
|
|
1541
|
+
function JsonFlowCard({ endpoint, flow }) {
|
|
1542
|
+
const mainNodes = flow.mainPath
|
|
1543
|
+
.map((nodeId) => flow.nodes.find((node) => node.id === nodeId))
|
|
1544
|
+
.filter((node) => Boolean(node));
|
|
1545
|
+
return (_jsxs("article", { className: "json-flow-card", children: [_jsxs("header", { children: [_jsx("span", { children: endpoint?.method ?? "FLOW" }), _jsxs("div", { children: [_jsx("strong", { title: endpoint?.path ?? flow.endpointId, children: endpoint?.path ?? flow.endpointId }), _jsxs("small", { children: [flow.nodes.length, " nodes \u00B7 ", flow.edges.length, " edges \u00B7 ", flow.subFlows.length, " sub flows"] })] })] }), _jsx("div", { className: "json-node-rail", children: (mainNodes.length > 0 ? mainNodes : flow.nodes.slice(0, 6)).map((node) => (_jsxs("span", { title: node.label, children: [_jsx("i", { children: node.type }), _jsx("strong", { children: node.label }), _jsxs("small", { children: [node.evidenceIds?.length ?? node.evidence?.length ?? 0, " evidence"] })] }, node.id))) })] }));
|
|
1546
|
+
}
|
|
1547
|
+
function jsonStats(data) {
|
|
1548
|
+
return {
|
|
1549
|
+
edgeCount: data.flows.reduce((sum, flow) => sum + flow.edges.length, 0),
|
|
1550
|
+
endpointCount: data.endpoints.length,
|
|
1551
|
+
evidenceCount: data.flows.reduce((sum, flow) => sum +
|
|
1552
|
+
flow.nodes.reduce((nodeSum, node) => nodeSum + (node.evidence?.length ?? node.evidenceIds?.length ?? 0), 0), 0),
|
|
1553
|
+
flowCount: data.flows.length,
|
|
1554
|
+
nodeCount: data.flows.reduce((sum, flow) => sum + flow.nodes.length, 0),
|
|
1555
|
+
pageCount: data.pages.length,
|
|
1556
|
+
warningCount: data.warnings.length
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
function methodCounts(data) {
|
|
1560
|
+
const counts = new Map();
|
|
1561
|
+
for (const endpoint of data.endpoints) {
|
|
1562
|
+
counts.set(endpoint.method, (counts.get(endpoint.method) ?? 0) + 1);
|
|
1563
|
+
}
|
|
1564
|
+
return [...counts.entries()].sort((first, second) => second[1] - first[1]);
|
|
1565
|
+
}
|
|
1566
|
+
function jsonTabLabel(tab) {
|
|
1567
|
+
if (tab === "overview")
|
|
1568
|
+
return "Overview";
|
|
1569
|
+
if (tab === "flows")
|
|
1570
|
+
return "Flows";
|
|
1571
|
+
return "Raw JSON";
|
|
1572
|
+
}
|
|
1573
|
+
function formatBytes(bytes) {
|
|
1574
|
+
if (bytes < 1024)
|
|
1575
|
+
return `${bytes} B`;
|
|
1576
|
+
if (bytes < 1024 * 1024)
|
|
1577
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1578
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1579
|
+
}
|
|
1580
|
+
function RequestContextPanel({ data, pageContext, record }) {
|
|
1581
|
+
const locale = useWorkspaceLocale();
|
|
1582
|
+
const layers = record ? diagramLayers(record) : [];
|
|
1583
|
+
const controller = findFirstLayer(layers, ["controller"]);
|
|
1584
|
+
const result = findFirstLayer(layers, ["result"]);
|
|
1585
|
+
const pageUrl = displayPageUrl(record, locale, pageContext);
|
|
1586
|
+
return (_jsxs("section", { className: "benefits-studio request-context-panel", "aria-label": t(locale, "selectedRequestLabel"), children: [_jsx("div", { className: "benefits-studio__main", children: _jsxs("div", { className: "benefits-studio__copy", children: [_jsxs("small", { children: [t(locale, "currentPage"), " \u00B7 ", data.projectName] }), _jsx("h2", { title: pageUrl, children: pageUrl }), _jsx("p", { children: record
|
|
1587
|
+
? requestContextDescription(record, locale)
|
|
1588
|
+
: t(locale, "noCurrentPageRequestsCopy") })] }) }), record ? (_jsxs("div", { className: "request-context-actions", "aria-label": t(locale, "requestEvidenceSummary"), children: [_jsxs("span", { children: [_jsx(OutcomeIcon, { record: record, size: 14 }), record.status ?? t(locale, "pending"), " ", outcomeStatusText(record, locale)] }), _jsxs("span", { children: [_jsx(Clock3, { size: 14 }), formatDuration(recordTotalDuration(record))] }), _jsxs("span", { children: [_jsx(Workflow, { size: 14 }), controller?.label ?? t(locale, "controllerPending")] }), _jsxs("span", { children: [_jsx(Flag, { size: 14 }), result?.label ?? outcomeLabel(record, locale)] })] })) : null] }));
|
|
1589
|
+
}
|
|
1590
|
+
function WorkspaceContent({ record, tab }) {
|
|
1591
|
+
if (tab === "summary") {
|
|
1592
|
+
return _jsx(SummaryFlowView, { record: record });
|
|
1593
|
+
}
|
|
1594
|
+
if (tab === "diagram") {
|
|
1595
|
+
return _jsx(DiagramFlowView, { record: record });
|
|
1596
|
+
}
|
|
1597
|
+
return _jsx(TimingWaterfallView, { record: record });
|
|
1598
|
+
}
|
|
1599
|
+
function TimingWaterfallView({ record }) {
|
|
1600
|
+
const locale = useWorkspaceLocale();
|
|
1601
|
+
const [timelineZoom, setTimelineZoom] = useState(1);
|
|
1602
|
+
const [focusSlowest, setFocusSlowest] = useState(false);
|
|
1603
|
+
const rows = compactTimingLayers(record);
|
|
1604
|
+
const total = recordTotalDuration(record, rows);
|
|
1605
|
+
const ticks = timelineTicks(total);
|
|
1606
|
+
const slowestLayer = rows
|
|
1607
|
+
.filter((layer) => !isUnprovenLayer(layer) && layer.type !== "api" && layer.type !== "result")
|
|
1608
|
+
.sort((a, b) => estimatedLayerDuration(b, total) - estimatedLayerDuration(a, total))[0];
|
|
1609
|
+
return (_jsxs("section", { className: "waterfall-card", "aria-label": t(locale, "flowTiming"), children: [_jsxs("div", { className: "waterfall-toolbar", children: [_jsxs("div", { className: "duration-chip", children: [t(locale, "totalDuration"), "\u00A0 ", _jsxs("strong", { children: [Math.round(total), " ms"] })] }), _jsxs("div", { className: "waterfall-toolbar__right", children: [_jsxs("div", { className: "zoom-group", children: [_jsx("button", { type: "button", onClick: () => setTimelineZoom(0.92), children: t(locale, "fit") }), _jsxs("button", { type: "button", onClick: () => setTimelineZoom(1), children: [Math.round(timelineZoom * 100), "%"] })] }), _jsxs("div", { className: "zoom-icons", children: [_jsx("button", { type: "button", "aria-label": t(locale, "focusSlowestSegment"), onClick: () => setFocusSlowest((value) => !value), children: _jsx(Search, { size: 17 }) }), _jsx("button", { type: "button", "aria-label": t(locale, "zoomIn"), onClick: () => setTimelineZoom((value) => Math.min(1.24, value + 0.08)), children: _jsx(Plus, { size: 17 }) })] })] })] }), _jsx(TimingOverview, { rows: rows, slowestLayer: slowestLayer, total: total }), _jsxs("div", { className: "waterfall-grid", style: { "--timeline-zoom": timelineZoom }, children: [_jsxs("div", { className: "waterfall-axis", children: [_jsx("span", { children: t(locale, "layer") }), _jsx("span", { children: t(locale, "node") }), _jsx("span", { children: t(locale, "duration") }), _jsx("div", { className: "waterfall-axis__timeline", style: { "--tick-count": ticks.length - 1 }, children: ticks.map((tick) => (_jsxs("strong", { style: { left: `${timelineX((tick / total) * 100)}%` }, children: [Math.round(tick), " ms"] }, tick))) })] }), rows.map((layer) => (_jsx(WaterfallRow, { focused: focusSlowest && layer.id === slowestLayer?.id, layer: layer, runtimeSource: record.runtimeSource, total: total, tickCount: ticks.length - 1 }, layer.id)))] }), _jsx(DurationLegend, {})] }));
|
|
1610
|
+
}
|
|
1611
|
+
function TimingOverview({ rows, slowestLayer, total }) {
|
|
1612
|
+
const locale = useWorkspaceLocale();
|
|
1613
|
+
const breakdown = timingBreakdownSegments(rows, total);
|
|
1614
|
+
return (_jsxs("section", { className: "timing-overview", "aria-label": t(locale, "totalDurationOverview"), children: [_jsxs("div", { className: "timing-overview__top", children: [_jsxs("div", { children: [_jsx("span", { children: t(locale, "totalDuration") }), _jsxs("strong", { children: ["0 ms to ", Math.round(total), " ms"] })] }), slowestLayer ? (_jsxs("p", { children: [t(locale, "slowestShownSegment"), ": ", _jsx("strong", { children: layerLabel(slowestLayer, locale) }), " ", _jsxs("span", { children: [Math.round(estimatedLayerDuration(slowestLayer, total)), " ms"] })] })) : null] }), _jsxs("div", { className: "timing-overview__breakdown", children: [_jsxs("div", { className: "timing-overview__scale", "aria-hidden": "true", children: [_jsx("strong", { children: "0" }), _jsxs("strong", { children: [Math.round(total), " ms"] })] }), _jsx("div", { className: "timing-breakdown-bar", "aria-label": t(locale, "totalDurationOverview"), children: breakdown.map((segment) => (_jsx("span", { className: `timing-breakdown-segment timing-breakdown-segment--${segment.type} ${segment.durationMs <= 0 ? "is-empty" : ""}`, style: { width: `${segment.width}%` }, title: `${segment.label}: ${Math.round(segment.durationMs)} ms` }, segment.type))) }), _jsx("div", { className: "timing-breakdown-legend", children: breakdown.map((segment) => (_jsxs("span", { className: segment.durationMs <= 0 ? "is-empty" : "", title: `${segment.label}: ${Math.round(segment.durationMs)} ms`, children: [_jsx("i", { className: `timing-breakdown-dot timing-breakdown-dot--${segment.type}` }), _jsx("b", { children: segment.label }), _jsxs("strong", { children: [Math.round(segment.durationMs), " ms"] })] }, segment.type))) })] })] }));
|
|
1615
|
+
}
|
|
1616
|
+
function WaterfallRow({ layer, focused, runtimeSource, tickCount, total }) {
|
|
1617
|
+
const locale = useWorkspaceLocale();
|
|
1618
|
+
const Icon = layerIcons[layer.type] ?? Circle;
|
|
1619
|
+
const muted = isUnprovenLayer(layer);
|
|
1620
|
+
const sourceMatched = isSourceMatchedLayer(layer);
|
|
1621
|
+
const visualType = visualLayerType(layer);
|
|
1622
|
+
const isResultMarker = layer.type === "result";
|
|
1623
|
+
const duration = layer.durationMs ?? estimatedLayerDuration(layer, total);
|
|
1624
|
+
const runtimeGroupCount = backendRuntimeGroupCount(layer);
|
|
1625
|
+
const left = layerOffset(layer, total);
|
|
1626
|
+
const width = muted
|
|
1627
|
+
? 0
|
|
1628
|
+
: duration <= 0
|
|
1629
|
+
? 0
|
|
1630
|
+
: Math.max(isResultMarker ? 0.8 : 1.8, Math.min(100 - left, (duration / total) * 100));
|
|
1631
|
+
const visualLeft = timelineX(left);
|
|
1632
|
+
const visualWidth = timelineWidth(left, width);
|
|
1633
|
+
return (_jsxs("div", { className: `waterfall-row waterfall-row--${visualType} ${muted ? "is-muted" : ""} ${sourceMatched ? "is-source-matched" : ""} ${focused ? "is-focused" : ""} ${layer.execution === "executed" || layer.execution === "inferred" ? "is-neutral" : ""}`, children: [_jsxs("div", { className: "waterfall-layer", children: [_jsx("span", { className: `layer-icon layer-icon--${visualType}`, children: _jsx(Icon, { size: 18 }) }), _jsx("span", { children: layerLabel(layer, locale) })] }), _jsxs("div", { className: "waterfall-node", children: [_jsx("strong", { children: layer.label }), _jsx("span", { children: layerSubtitle(layer, locale, runtimeSource) })] }), _jsx("div", { className: "waterfall-duration", children: muted ? (_jsxs(_Fragment, { children: [_jsx("strong", { children: "-" }), _jsx("small", { children: t(locale, "notProven") })] })) : (_jsxs(_Fragment, { children: [_jsx("strong", { children: durationValueLabel(layer, duration) }), _jsx("small", { children: durationCaption(layer, duration, total, locale, runtimeSource, runtimeGroupCount) })] })) }), _jsxs("div", { className: "waterfall-track", style: { "--tick-count": tickCount }, children: [_jsx("span", { className: muted
|
|
1634
|
+
? "waterfall-not-proven-label"
|
|
1635
|
+
: `waterfall-bar waterfall-bar--${visualType}${isResultMarker ? " is-marker" : ""}`, style: muted ? undefined : { left: `${visualLeft}%`, width: `${visualWidth}%` }, children: muted ? t(locale, "knownBySourceOnly") : null }), isDecisionLayer(layer) && layer.execution !== "executed" ? (_jsx("span", { className: "row-badge row-badge--red", children: t(locale, "bottleneck") })) : null, sourceMatched && focused ? (_jsx("span", { className: "row-badge row-badge--blue", children: t(locale, "slowestSourceSegment") })) : null, layer.type === "result" && layer.execution === "blocked" ? (_jsx("span", { className: "row-badge row-badge--orange", children: t(locale, "blocked") })) : null] })] }));
|
|
1636
|
+
}
|
|
1637
|
+
function SummaryFlowView({ record }) {
|
|
1638
|
+
const locale = useWorkspaceLocale();
|
|
1639
|
+
const rows = summaryRows(record);
|
|
1640
|
+
const matchedRows = rows.filter((layer) => !isUnprovenLayer(layer));
|
|
1641
|
+
const unprovenRows = rows.filter((layer) => isUnprovenLayer(layer));
|
|
1642
|
+
return (_jsx("section", { className: "summary-view", "aria-label": t(locale, "flowSummary"), children: _jsxs("div", { className: "summary-panel", children: [_jsxs("div", { className: "summary-panel__header", children: [_jsxs("div", { children: [_jsx("h2", { children: t(locale, "observedSourceMatchedPath") }), _jsx("p", { children: t(locale, "summaryMatchedCopy") })] }), _jsxs("span", { children: [matchedRows.length, " ", t(locale, "layers")] })] }), _jsx("div", { className: "summary-path-list", children: matchedRows.map((layer, index) => (_jsx(SummaryPathRow, { index: index + 1, layer: layer, runtimeSource: record.runtimeSource }, layer.id))) }), unprovenRows.length > 0 ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "summary-panel__subheader", children: [_jsx("h3", { children: t(locale, "knownDownstreamPath") }), _jsxs("span", { children: [unprovenRows.length, " ", t(locale, "layers")] })] }), _jsx("div", { className: "summary-path-list summary-path-list--muted", children: unprovenRows.map((layer, index) => (_jsx(SummaryPathRow, { index: matchedRows.length + index + 1, layer: layer, runtimeSource: record.runtimeSource }, layer.id))) })] })) : null, _jsx("p", { className: "summary-note", children: t(locale, "summaryNote") })] }) }));
|
|
1643
|
+
}
|
|
1644
|
+
function SummaryPathRow({ index, layer, runtimeSource }) {
|
|
1645
|
+
const locale = useWorkspaceLocale();
|
|
1646
|
+
const Icon = layerIcons[layer.type] ?? Circle;
|
|
1647
|
+
const visualType = visualLayerType(layer);
|
|
1648
|
+
const muted = isUnprovenLayer(layer);
|
|
1649
|
+
return (_jsxs("article", { className: `summary-path-row ${muted ? "is-muted" : ""}`, children: [_jsx("span", { className: `summary-step summary-step--${visualType}`, children: index }), _jsx("span", { className: `layer-icon layer-icon--${visualType}`, children: _jsx(Icon, { size: 18 }) }), _jsxs("div", { children: [_jsx("small", { children: layerLabel(layer, locale) }), _jsx("strong", { children: layer.label }), _jsx("p", { children: layerSubtitle(layer, locale, runtimeSource) })] }), _jsx("span", { className: `summary-status summary-status--${summaryStatusTone(layer)}`, children: summaryStatusLabel(layer, locale) })] }));
|
|
1650
|
+
}
|
|
1651
|
+
function DiagramFlowView({ record }) {
|
|
1652
|
+
const locale = useWorkspaceLocale();
|
|
1653
|
+
const [zoom, setZoom] = useState(0.82);
|
|
1654
|
+
const nodeTypes = useMemo(() => ({ anlyxNode: LayeredFlowNode }), []);
|
|
1655
|
+
const edgeTypes = useMemo(() => ({ anlyxSmooth: LayeredSmoothEdge }), []);
|
|
1656
|
+
const model = useMemo(() => buildLayeredReactFlowDiagram(record, locale), [record, locale]);
|
|
1657
|
+
useEffect(() => {
|
|
1658
|
+
setZoom(0.82);
|
|
1659
|
+
}, [record.id]);
|
|
1660
|
+
return (_jsxs("section", { className: "diagram-canvas-card", "aria-label": t(locale, "flowDiagram"), children: [_jsxs("div", { className: "diagram-canvas-toolbar", children: [_jsxs("div", { className: "diagram-legend", children: [_jsxs("span", { children: [_jsx("i", { className: "solid-line" }), t(locale, "observedSourceMatchedLegend")] }), _jsxs("span", { children: [_jsx("i", { className: "dashed-line" }), t(locale, "knownDownstreamLegend")] }), _jsxs("span", { children: [_jsx(Circle, { size: 12 }), t(locale, "entryPoint")] })] }), _jsxs("div", { className: "diagram-controls", children: [_jsxs("button", { type: "button", onClick: () => setZoom(0.82), children: [_jsx(Gauge, { size: 16 }), t(locale, "fitView")] }), _jsxs("button", { type: "button", onClick: () => setZoom(1), children: [_jsx(RotateCw, { size: 16 }), t(locale, "reset")] }), _jsxs("div", { children: [_jsx("button", { type: "button", "aria-label": t(locale, "zoomOut"), onClick: () => setZoom((value) => Math.max(0.72, value - 0.1)), children: _jsx(Minus, { size: 16 }) }), _jsxs("span", { children: [Math.round(zoom * 100), "%"] }), _jsx("button", { type: "button", "aria-label": t(locale, "zoomIn"), onClick: () => setZoom((value) => Math.min(1.2, value + 0.1)), children: _jsx(Plus, { size: 16 }) })] })] })] }), _jsx("div", { className: "diagram-canvas diagram-canvas--layered", style: {
|
|
1661
|
+
"--diagram-stage-height": `${Math.ceil(model.canvasHeight * zoom + 72)}px`
|
|
1662
|
+
}, children: _jsxs("div", { className: `layered-diagram layered-diagram--react-flow ${zoom === 0.82 ? "layered-diagram--fit" : ""}`, style: {
|
|
1663
|
+
"--layered-column-height": `${model.canvasHeight - 50}px`,
|
|
1664
|
+
transform: `scale(${zoom})`
|
|
1665
|
+
}, children: [model.layers.map((layer) => (_jsx(LayeredDiagramColumnBackdrop, { hasSecondaryNodes: model.nodes.some((node) => node.data.node.layer === layer && !node.data.node.isMainPath), layer: layer, locale: locale }, layer))), _jsx(ReactFlow, { className: "layered-react-flow", edgeTypes: edgeTypes, edges: model.edges, fitView: false, maxZoom: 1, minZoom: 1, nodes: model.nodes, nodesConnectable: false, nodesDraggable: false, nodeTypes: nodeTypes, panOnDrag: false, preventScrolling: false, proOptions: { hideAttribution: true }, zoomOnDoubleClick: false, zoomOnPinch: false, zoomOnScroll: false })] }) }), _jsxs("div", { className: "layered-diagram-status", children: [_jsxs("span", { children: [_jsx("i", { className: "status-dot status-dot--success" }), locale === "ko" ? "성공" : "Success"] }), _jsxs("span", { children: [_jsx("i", { className: "status-dot status-dot--warning" }), locale === "ko" ? "주의" : "Warning"] }), _jsxs("span", { children: [_jsx("i", { className: "status-dot status-dot--error" }), locale === "ko" ? "오류" : "Error"] }), _jsxs("span", { children: [_jsx("i", { className: "dashed-line" }), locale === "ko" ? "미확인" : "not proven"] }), _jsxs("strong", { children: [t(locale, "totalDuration"), ": ", recordDuration(record), " ms"] })] })] }));
|
|
1666
|
+
}
|
|
1667
|
+
const layeredDiagramLayers = [
|
|
1668
|
+
"browser",
|
|
1669
|
+
"api",
|
|
1670
|
+
"application",
|
|
1671
|
+
"data",
|
|
1672
|
+
"response"
|
|
1673
|
+
];
|
|
1674
|
+
const layeredLayout = {
|
|
1675
|
+
canvasHeight: 610,
|
|
1676
|
+
columnWidth: 220,
|
|
1677
|
+
nodeWidth: 180,
|
|
1678
|
+
nodeHeight: 148,
|
|
1679
|
+
mainStartY: 98,
|
|
1680
|
+
mainGap: 172,
|
|
1681
|
+
secondaryStartY: 430,
|
|
1682
|
+
secondaryGap: 158,
|
|
1683
|
+
layerX: {
|
|
1684
|
+
browser: 0,
|
|
1685
|
+
api: 260,
|
|
1686
|
+
application: 520,
|
|
1687
|
+
data: 860,
|
|
1688
|
+
response: 1120
|
|
1689
|
+
}
|
|
1690
|
+
};
|
|
1691
|
+
function LayeredDiagramColumnBackdrop({ hasSecondaryNodes, layer, locale }) {
|
|
1692
|
+
const heading = layeredLayerMeta(layer, locale);
|
|
1693
|
+
const Icon = heading.icon;
|
|
1694
|
+
return (_jsxs("section", { className: `layered-column layered-column--${layer}`, style: { left: layeredLayout.layerX[layer] }, children: [_jsxs("header", { className: "layered-column__header", children: [_jsx("span", { children: _jsx(Icon, { size: 22 }) }), _jsxs("div", { children: [_jsx("strong", { children: heading.title }), _jsx("small", { children: heading.subtitle })] })] }), hasSecondaryNodes ? (_jsx("div", { className: "layered-column__divider layered-column__divider--absolute", children: locale === "ko" ? "알려졌지만 미확인" : "Known but not proven" })) : null] }));
|
|
1695
|
+
}
|
|
1696
|
+
function LayeredFlowNode({ data }) {
|
|
1697
|
+
return _jsx(LayeredNodeCard, { node: data.node });
|
|
1698
|
+
}
|
|
1699
|
+
function LayeredNodeCard({ node }) {
|
|
1700
|
+
const Icon = layeredNodeIcon(node);
|
|
1701
|
+
return (_jsxs("article", { className: `layered-node layered-node--${node.layer} layered-node--${node.status} ${node.isMainPath ? "is-main-path" : "is-secondary-path"} ${node.kind === "action" ? "is-entry-point" : ""}`, title: `${node.title} · ${node.subtitle}`, children: [_jsx(Handle, { className: "anlyx-flow-handle", id: "left", position: Position.Left, type: "target" }), _jsx(Handle, { className: "anlyx-flow-handle", id: "right", position: Position.Right, type: "source" }), _jsx(Handle, { className: "anlyx-flow-handle", id: "top", position: Position.Top, type: "target" }), _jsx(Handle, { className: "anlyx-flow-handle", id: "top", position: Position.Top, type: "source" }), _jsx(Handle, { className: "anlyx-flow-handle", id: "bottom", position: Position.Bottom, type: "source" }), _jsx(Handle, { className: "anlyx-flow-handle", id: "bottom", position: Position.Bottom, type: "target" }), node.kind === "action" ? _jsx("span", { className: "layered-node__entry", "aria-hidden": "true" }) : null, _jsx("span", { className: `layered-node__icon layered-node__icon--${node.layer}`, children: _jsx(Icon, { size: 20 }) }), _jsx("span", { className: "layered-node__state", children: node.status === "not_proven" ? _jsx(Minus, { size: 13 }) : _jsx(Check, { size: 13 }) }), _jsx("strong", { children: node.title }), _jsx("small", { children: node.subtitle }), _jsxs("div", { className: "layered-node__meta", children: [_jsx("span", { children: node.durationMs !== undefined ? `${node.durationMs} ms` : "—" }), _jsx("em", { children: layeredDiagramNodeStatusLabel(node.status) })] })] }));
|
|
1702
|
+
}
|
|
1703
|
+
function LayeredSmoothEdge({ data, sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY }) {
|
|
1704
|
+
const [path] = getSmoothStepPath({
|
|
1705
|
+
borderRadius: 18,
|
|
1706
|
+
offset: 26,
|
|
1707
|
+
sourcePosition,
|
|
1708
|
+
sourceX,
|
|
1709
|
+
sourceY,
|
|
1710
|
+
targetPosition,
|
|
1711
|
+
targetX,
|
|
1712
|
+
targetY
|
|
1713
|
+
});
|
|
1714
|
+
const kind = data?.kind ?? "executed";
|
|
1715
|
+
return (_jsxs(_Fragment, { children: [_jsx("path", { className: `anlyx-flow-edge-halo anlyx-flow-edge-halo--${kind}`, d: path }), _jsx(BaseEdge, { className: `anlyx-flow-edge anlyx-flow-edge--${kind}`, path: path })] }));
|
|
1716
|
+
}
|
|
1717
|
+
function buildLayeredReactFlowDiagram(record, locale) {
|
|
1718
|
+
const diagramNodes = assignLayeredNodePositions(layeredDiagramLayersForRecord(record).map((layer) => toLayeredDiagramNode(layer, record, locale)));
|
|
1719
|
+
const diagramEdges = buildLayeredDiagramEdges(diagramNodes);
|
|
1720
|
+
return {
|
|
1721
|
+
layers: layeredDiagramLayers,
|
|
1722
|
+
canvasHeight: diagramCanvasHeight(diagramNodes),
|
|
1723
|
+
nodes: diagramNodes.map((node) => ({
|
|
1724
|
+
id: node.id,
|
|
1725
|
+
type: "anlyxNode",
|
|
1726
|
+
position: { x: node.x, y: node.y },
|
|
1727
|
+
data: { node },
|
|
1728
|
+
draggable: false,
|
|
1729
|
+
selectable: false
|
|
1730
|
+
})),
|
|
1731
|
+
edges: diagramEdges.map((edge) => {
|
|
1732
|
+
const from = diagramNodes.find((node) => node.id === edge.from);
|
|
1733
|
+
const to = diagramNodes.find((node) => node.id === edge.to);
|
|
1734
|
+
const handles = from && to ? reactFlowHandlesForEdge(from, to) : {};
|
|
1735
|
+
return {
|
|
1736
|
+
id: edge.id,
|
|
1737
|
+
source: edge.from,
|
|
1738
|
+
target: edge.to,
|
|
1739
|
+
type: "anlyxSmooth",
|
|
1740
|
+
data: { kind: edge.kind },
|
|
1741
|
+
focusable: false,
|
|
1742
|
+
selectable: false,
|
|
1743
|
+
...handles
|
|
1744
|
+
};
|
|
1745
|
+
})
|
|
1746
|
+
};
|
|
1747
|
+
}
|
|
1748
|
+
function layeredDiagramLayersForRecord(record) {
|
|
1749
|
+
const layers = diagramLayers(record).filter((layer) => layer.type !== "page");
|
|
1750
|
+
const fallback = layers.length > 0 ? layers : timingLayers(record);
|
|
1751
|
+
const hasBlocked = hasBlockedOutcome(record);
|
|
1752
|
+
const order = hasBlocked
|
|
1753
|
+
? [
|
|
1754
|
+
"action",
|
|
1755
|
+
"api",
|
|
1756
|
+
"controller",
|
|
1757
|
+
"auth",
|
|
1758
|
+
"decision",
|
|
1759
|
+
"result",
|
|
1760
|
+
"service",
|
|
1761
|
+
"repository",
|
|
1762
|
+
"database"
|
|
1763
|
+
]
|
|
1764
|
+
: [
|
|
1765
|
+
"action",
|
|
1766
|
+
"api",
|
|
1767
|
+
"controller",
|
|
1768
|
+
"auth",
|
|
1769
|
+
"decision",
|
|
1770
|
+
"service",
|
|
1771
|
+
"repository",
|
|
1772
|
+
"database",
|
|
1773
|
+
"result"
|
|
1774
|
+
];
|
|
1775
|
+
return order
|
|
1776
|
+
.map((type) => fallback.find((layer) => layer.type === type))
|
|
1777
|
+
.filter((layer) => Boolean(layer));
|
|
1778
|
+
}
|
|
1779
|
+
function diagramCanvasHeight(nodes) {
|
|
1780
|
+
return Math.max(layeredLayout.canvasHeight, ...nodes.map((node) => node.y + layeredLayout.nodeHeight + 110));
|
|
1781
|
+
}
|
|
1782
|
+
function assignLayeredNodePositions(nodes) {
|
|
1783
|
+
const mainSlotByLayer = new Map();
|
|
1784
|
+
const secondarySlotByLayer = new Map();
|
|
1785
|
+
return nodes.map((node) => {
|
|
1786
|
+
const slotMap = node.isMainPath ? mainSlotByLayer : secondarySlotByLayer;
|
|
1787
|
+
const slot = slotMap.get(node.layer) ?? 0;
|
|
1788
|
+
const y = node.isMainPath
|
|
1789
|
+
? layeredLayout.mainStartY + slot * layeredLayout.mainGap
|
|
1790
|
+
: layeredLayout.secondaryStartY + slot * layeredLayout.secondaryGap;
|
|
1791
|
+
slotMap.set(node.layer, slot + 1);
|
|
1792
|
+
return {
|
|
1793
|
+
...node,
|
|
1794
|
+
slot,
|
|
1795
|
+
x: layeredLayout.layerX[node.layer] +
|
|
1796
|
+
(layeredLayout.columnWidth - layeredLayout.nodeWidth) / 2,
|
|
1797
|
+
y
|
|
1798
|
+
};
|
|
1799
|
+
});
|
|
1800
|
+
}
|
|
1801
|
+
function toLayeredDiagramNode(layer, record, locale) {
|
|
1802
|
+
const status = layeredDiagramNodeStatus(layer, record);
|
|
1803
|
+
return {
|
|
1804
|
+
id: layer.id,
|
|
1805
|
+
sourceLayer: layer,
|
|
1806
|
+
layer: diagramLayerForFlowLayer(layer.type),
|
|
1807
|
+
kind: layer.type,
|
|
1808
|
+
title: layeredDiagramTitle(layer, record, locale),
|
|
1809
|
+
subtitle: layeredDiagramSubtitle(layer, record),
|
|
1810
|
+
status,
|
|
1811
|
+
edgeKind: edgeKindForLayer(layer, status),
|
|
1812
|
+
isMainPath: !isUnprovenLayer(layer),
|
|
1813
|
+
slot: 0,
|
|
1814
|
+
x: 0,
|
|
1815
|
+
y: 0,
|
|
1816
|
+
...(layer.durationMs !== undefined ? { durationMs: Math.round(layer.durationMs) } : {})
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
function buildLayeredDiagramEdges(nodes) {
|
|
1820
|
+
const mainNodes = nodes.filter((node) => node.isMainPath).sort(compareLayeredNodes);
|
|
1821
|
+
const secondaryNodes = nodes.filter((node) => !node.isMainPath).sort(compareLayeredNodes);
|
|
1822
|
+
const edges = [];
|
|
1823
|
+
for (let index = 0; index < mainNodes.length - 1; index += 1) {
|
|
1824
|
+
const from = mainNodes[index];
|
|
1825
|
+
const to = mainNodes[index + 1];
|
|
1826
|
+
if (from && to) {
|
|
1827
|
+
edges.push({
|
|
1828
|
+
id: `${from.id}->${to.id}`,
|
|
1829
|
+
from: from.id,
|
|
1830
|
+
to: to.id,
|
|
1831
|
+
kind: edgeKindBetween(from, to)
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
const anchor = mainNodes.find((node) => node.layer === "application" && node.kind !== "controller") ??
|
|
1836
|
+
mainNodes.find((node) => node.layer === "application") ??
|
|
1837
|
+
mainNodes.find((node) => node.layer === "api");
|
|
1838
|
+
if (anchor) {
|
|
1839
|
+
for (const node of secondaryNodes) {
|
|
1840
|
+
edges.push({
|
|
1841
|
+
id: `${anchor.id}->${node.id}`,
|
|
1842
|
+
from: anchor.id,
|
|
1843
|
+
to: node.id,
|
|
1844
|
+
kind: "not_proven"
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
return edges;
|
|
1849
|
+
}
|
|
1850
|
+
function compareLayeredNodes(first, second) {
|
|
1851
|
+
const layerDelta = layeredDiagramLayers.indexOf(first.layer) - layeredDiagramLayers.indexOf(second.layer);
|
|
1852
|
+
if (layerDelta !== 0)
|
|
1853
|
+
return layerDelta;
|
|
1854
|
+
return layerOrderIndex(first.kind) - layerOrderIndex(second.kind);
|
|
1855
|
+
}
|
|
1856
|
+
function reactFlowHandlesForEdge(from, to) {
|
|
1857
|
+
if (from.layer === to.layer) {
|
|
1858
|
+
return to.y > from.y
|
|
1859
|
+
? { sourceHandle: "bottom", targetHandle: "top" }
|
|
1860
|
+
: { sourceHandle: "top", targetHandle: "bottom" };
|
|
1861
|
+
}
|
|
1862
|
+
return { sourceHandle: "right", targetHandle: "left" };
|
|
1863
|
+
}
|
|
1864
|
+
function edgeKindBetween(from, to) {
|
|
1865
|
+
if (from.status === "blocked" || to.status === "blocked")
|
|
1866
|
+
return "blocked";
|
|
1867
|
+
if (from.status === "inferred" || to.status === "inferred")
|
|
1868
|
+
return "inferred";
|
|
1869
|
+
if (from.status === "source_matched" || to.status === "source_matched")
|
|
1870
|
+
return "source_matched";
|
|
1871
|
+
return "executed";
|
|
1872
|
+
}
|
|
1873
|
+
function edgeKindForLayer(layer, status) {
|
|
1874
|
+
if (status === "blocked")
|
|
1875
|
+
return "blocked";
|
|
1876
|
+
if (status === "inferred")
|
|
1877
|
+
return "inferred";
|
|
1878
|
+
if (isUnprovenLayer(layer) || status === "not_proven")
|
|
1879
|
+
return "not_proven";
|
|
1880
|
+
if (layer.type !== "action" && layer.type !== "api" && layer.type !== "result") {
|
|
1881
|
+
return "source_matched";
|
|
1882
|
+
}
|
|
1883
|
+
return "executed";
|
|
1884
|
+
}
|
|
1885
|
+
function diagramLayerForFlowLayer(layer) {
|
|
1886
|
+
if (layer === "action")
|
|
1887
|
+
return "browser";
|
|
1888
|
+
if (layer === "api")
|
|
1889
|
+
return "api";
|
|
1890
|
+
if (layer === "repository" || layer === "database")
|
|
1891
|
+
return "data";
|
|
1892
|
+
if (layer === "result")
|
|
1893
|
+
return "response";
|
|
1894
|
+
return "application";
|
|
1895
|
+
}
|
|
1896
|
+
function layeredDiagramNodeStatus(layer, record) {
|
|
1897
|
+
if (isUnprovenLayer(layer))
|
|
1898
|
+
return "not_proven";
|
|
1899
|
+
if (layer.execution === "blocked")
|
|
1900
|
+
return "blocked";
|
|
1901
|
+
if (layer.execution === "inferred" || layer.evidenceLevel === "inferred")
|
|
1902
|
+
return "inferred";
|
|
1903
|
+
if (layer.type === "result" && outcomeTone(record) === "success")
|
|
1904
|
+
return "success";
|
|
1905
|
+
if (layer.type === "action" || layer.type === "api")
|
|
1906
|
+
return "observed";
|
|
1907
|
+
if (layer.type !== "result")
|
|
1908
|
+
return "source_matched";
|
|
1909
|
+
return "matched";
|
|
1910
|
+
}
|
|
1911
|
+
function layeredDiagramTitle(layer, record, locale) {
|
|
1912
|
+
if (layer.type === "action")
|
|
1913
|
+
return locale === "ko" ? "사용자 액션" : "User Action";
|
|
1914
|
+
if (layer.type === "api")
|
|
1915
|
+
return locale === "ko" ? "HTTP 요청" : "HTTP Request";
|
|
1916
|
+
if (layer.type === "result")
|
|
1917
|
+
return outcomeTone(record) === "success" ? "OK" : outcomeLabel(record, locale);
|
|
1918
|
+
return layerLabel(layer, locale);
|
|
1919
|
+
}
|
|
1920
|
+
function layeredDiagramSubtitle(layer, record) {
|
|
1921
|
+
if (layer.type === "action")
|
|
1922
|
+
return record.action?.label ?? layer.label;
|
|
1923
|
+
if (layer.type === "api")
|
|
1924
|
+
return `${record.method} ${record.path}`;
|
|
1925
|
+
if (layer.type === "result")
|
|
1926
|
+
return layer.label;
|
|
1927
|
+
return layer.label;
|
|
1928
|
+
}
|
|
1929
|
+
function layeredLayerMeta(layer, locale) {
|
|
1930
|
+
const ko = locale === "ko";
|
|
1931
|
+
if (layer === "browser") {
|
|
1932
|
+
return {
|
|
1933
|
+
icon: MousePointerClick,
|
|
1934
|
+
title: ko ? "브라우저" : "Browser",
|
|
1935
|
+
subtitle: ko ? "사용자 상호작용" : "User interaction"
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
if (layer === "api") {
|
|
1939
|
+
return {
|
|
1940
|
+
icon: Network,
|
|
1941
|
+
title: "API",
|
|
1942
|
+
subtitle: ko ? "요청과 라우팅" : "Edge & routing"
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
if (layer === "application") {
|
|
1946
|
+
return {
|
|
1947
|
+
icon: Workflow,
|
|
1948
|
+
title: ko ? "애플리케이션" : "Application",
|
|
1949
|
+
subtitle: ko ? "비즈니스 로직" : "Business logic"
|
|
1950
|
+
};
|
|
1951
|
+
}
|
|
1952
|
+
if (layer === "data") {
|
|
1953
|
+
return {
|
|
1954
|
+
icon: Database,
|
|
1955
|
+
title: ko ? "데이터" : "Data",
|
|
1956
|
+
subtitle: ko ? "저장소 계층" : "Persistence layer"
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
return {
|
|
1960
|
+
icon: Flag,
|
|
1961
|
+
title: ko ? "응답" : "Response",
|
|
1962
|
+
subtitle: ko ? "클라이언트로 반환" : "Back to client"
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function layeredNodeIcon(node) {
|
|
1966
|
+
if (node.kind === "action")
|
|
1967
|
+
return MousePointerClick;
|
|
1968
|
+
if (node.kind === "api")
|
|
1969
|
+
return Network;
|
|
1970
|
+
if (node.kind === "auth" || node.kind === "decision")
|
|
1971
|
+
return Lock;
|
|
1972
|
+
if (node.kind === "service")
|
|
1973
|
+
return Layers3;
|
|
1974
|
+
if (node.kind === "repository")
|
|
1975
|
+
return Box;
|
|
1976
|
+
if (node.kind === "database")
|
|
1977
|
+
return Database;
|
|
1978
|
+
if (node.kind === "result")
|
|
1979
|
+
return Flag;
|
|
1980
|
+
if (node.kind === "controller")
|
|
1981
|
+
return FileCode2;
|
|
1982
|
+
return Circle;
|
|
1983
|
+
}
|
|
1984
|
+
function layeredDiagramNodeStatusLabel(status) {
|
|
1985
|
+
if (status === "observed")
|
|
1986
|
+
return "observed";
|
|
1987
|
+
if (status === "source_matched")
|
|
1988
|
+
return "source matched";
|
|
1989
|
+
if (status === "inferred")
|
|
1990
|
+
return "inferred";
|
|
1991
|
+
if (status === "not_proven")
|
|
1992
|
+
return "not proven";
|
|
1993
|
+
if (status === "success")
|
|
1994
|
+
return "success";
|
|
1995
|
+
if (status === "blocked")
|
|
1996
|
+
return "blocked";
|
|
1997
|
+
return "matched";
|
|
1998
|
+
}
|
|
1999
|
+
function recordTotalDuration(record, rows) {
|
|
2000
|
+
const baseDuration = record.durationMs ?? record.duration ?? 0;
|
|
2001
|
+
const layerEnd = Math.max(0, ...(rows ?? timingLayers(record)).map((layer) => (layer.startOffsetMs ?? 0) + Math.max(1, layer.durationMs ?? 0)));
|
|
2002
|
+
return Math.max(1, Math.round(Math.max(baseDuration, layerEnd)));
|
|
2003
|
+
}
|
|
2004
|
+
function recordDuration(record) {
|
|
2005
|
+
return Math.round(recordTotalDuration(record));
|
|
2006
|
+
}
|
|
2007
|
+
function FlowInspector({ onSelect, pageContext, record, records, selectedId }) {
|
|
2008
|
+
const locale = useWorkspaceLocale();
|
|
2009
|
+
const evidence = useMemo(() => record?.evidence ?? [], [record]);
|
|
2010
|
+
const recentRecords = useMemo(() => mergeRecentRecords(records, selectedId), [records, selectedId]);
|
|
2011
|
+
const coverage = record
|
|
2012
|
+
? evidenceCoverage(record)
|
|
2013
|
+
: { browser: 0, backend: 0, source: 0, notProven: 0 };
|
|
2014
|
+
return (_jsxs("aside", { className: "flow-inspector", "aria-label": t(locale, "evidenceInspector"), children: [_jsx("div", { className: "inspector-title", children: _jsx("h2", { children: t(locale, "anlyxFlow") }) }), _jsx(InspectorSection, { title: t(locale, "recentRequests"), count: String(recentRecords.length), children: _jsx(RecentEventsList, { records: recentRecords, selectedId: selectedId, onSelect: onSelect }) }), record ? (_jsxs(_Fragment, { children: [_jsx(InspectorSection, { title: t(locale, "request"), children: _jsxs("dl", { className: "request-dl", children: [_jsxs("div", { children: [_jsx("dt", { children: t(locale, "method") }), _jsx("dd", { children: _jsx("span", { className: "method-pill", children: record.method }) })] }), _jsxs("div", { children: [_jsx("dt", { children: t(locale, "path") }), _jsx("dd", { children: shortPath(record.path) })] })] }) }), _jsxs(InspectorSection, { title: t(locale, "outcome"), children: [_jsxs("span", { className: `outcome-pill outcome-pill--${outcomeTone(record)}`, children: [record.status ?? "—", "\u00A0\u00A0", outcomeStatusText(record, locale)] }), _jsxs("p", { children: [_jsx("strong", { children: outcomeLabel(record, locale) }), outcomeDescription(record, locale)] })] }), _jsx(InspectorSection, { title: t(locale, "totalDuration"), children: _jsxs("p", { className: "metric-line", children: [_jsx(Clock3, { size: 17 }), _jsx("strong", { children: formatDuration(recordTotalDuration(record)) })] }) }), _jsxs(InspectorSection, { title: t(locale, "mappingConfidence"), children: [_jsxs("div", { className: "confidence-row", children: [_jsx("span", { className: "confidence-chip", children: confidenceLabel(record.confidence, locale) }), _jsx(ConfidenceBars, {})] }), _jsx("p", { children: confidenceDescription(record, locale) })] }), _jsx(InspectorSection, { title: t(locale, "evidenceUsed"), count: String(evidence.length), children: _jsx("ul", { className: "evidence-list", children: evidence.slice(0, 5).map((item) => (_jsxs("li", { children: [_jsx(Check, { size: 14 }), translateEvidence(item, locale)] }, item))) }) }), _jsxs(InspectorSection, { title: t(locale, "coverage"), children: [_jsxs("div", { className: "layers-bar", "aria-hidden": "true", children: [_jsx("span", { style: { flex: Math.max(coverage.browser, 1) } }), _jsx("span", { style: { flex: Math.max(coverage.backend, 1) } }), _jsx("span", { style: { flex: Math.max(coverage.source, 1) } }), _jsx("span", { style: { flex: Math.max(coverage.notProven, 1) } })] }), _jsxs("small", { children: [t(locale, "browser"), " ", coverage.browser, " \u00B7 ", t(locale, "backendCoverage"), " ", coverage.backend, " \u00B7 ", t(locale, "source"), " ", coverage.source, " \u00B7", " ", t(locale, "notProven"), " ", coverage.notProven] })] }), _jsx(InspectorSection, { title: t(locale, "traceNote"), children: _jsx("p", { children: flowNote(record, locale) }) })] })) : (_jsx(InspectorSection, { title: t(locale, "request"), children: _jsx("p", { className: "events-empty", children: pageContext ? t(locale, "noCurrentPageRequestsCopy") : t(locale, "keepWorkspaceOpen") }) }))] }));
|
|
2015
|
+
}
|
|
2016
|
+
function RecentEventsList({ onSelect, records, selectedId }) {
|
|
2017
|
+
const locale = useWorkspaceLocale();
|
|
2018
|
+
const primaryRecords = records.filter(isPrimaryRecord).slice(0, 5);
|
|
2019
|
+
const backgroundRecords = records.filter((record) => !isPrimaryRecord(record)).slice(0, 4);
|
|
2020
|
+
if (records.length === 0) {
|
|
2021
|
+
return _jsx("p", { className: "events-empty", children: t(locale, "noCurrentPageRequestsCopy") });
|
|
2022
|
+
}
|
|
2023
|
+
return (_jsxs("div", { className: "recent-events", "aria-label": "Recent events", children: [_jsx("p", { className: "recent-events-scope", children: t(locale, "currentRequestWindow") }), primaryRecords.map((record) => (_jsx(RecentEventButton, { onSelect: onSelect, record: record, selectedId: selectedId }, record.id))), backgroundRecords.length > 0 ? (_jsxs("div", { className: "recent-events-background", children: [_jsx("p", { children: t(locale, "background") }), backgroundRecords.map((record) => (_jsx(RecentEventButton, { background: true, onSelect: onSelect, record: record, selectedId: selectedId }, record.id)))] })) : null] }));
|
|
2024
|
+
}
|
|
2025
|
+
function RecentEventButton({ background = false, onSelect, record, selectedId }) {
|
|
2026
|
+
return (_jsxs("button", { className: `${record.id === selectedId ? "is-selected" : ""} ${background ? "is-background" : ""}`, type: "button", onClick: () => onSelect(record.id), children: [_jsx("span", { children: record.method }), _jsx("strong", { children: shortPath(record.path) }), _jsxs("small", { children: [record.status ?? "pending", " \u00B7 ", recordTotalDuration(record), " ms", record.occurrenceCount && record.occurrenceCount > 1
|
|
2027
|
+
? ` · ${record.occurrenceCount}x`
|
|
2028
|
+
: ""] })] }));
|
|
2029
|
+
}
|
|
2030
|
+
function EmptyWorkspace({ pageContext }) {
|
|
2031
|
+
const locale = useWorkspaceLocale();
|
|
2032
|
+
const pageUrl = pageContext ? normalizeDisplayUrl(pageContext.pageUrl) : undefined;
|
|
2033
|
+
return (_jsxs("section", { className: "waterfall-card empty-workspace-card", "aria-label": t(locale, "waitingForBrowserRequests"), children: [_jsx("span", { children: pageContext ? t(locale, "currentPage") : t(locale, "liveCaptureReady") }), _jsx("h2", { children: pageContext ? t(locale, "noCurrentPageRequests") : t(locale, "noRequestSelected") }), _jsx("p", { children: pageContext
|
|
2034
|
+
? `${pageUrl}: ${t(locale, "noCurrentPageRequestsCopy")}`
|
|
2035
|
+
: t(locale, "emptyWorkspaceCopy") })] }));
|
|
2036
|
+
}
|
|
2037
|
+
function InspectorSection({ children, count, title }) {
|
|
2038
|
+
return (_jsxs("section", { className: "inspector-section", children: [_jsxs("h3", { children: [title, count ? _jsx("span", { children: count }) : null] }), children] }));
|
|
2039
|
+
}
|
|
2040
|
+
function DurationLegend() {
|
|
2041
|
+
const locale = useWorkspaceLocale();
|
|
2042
|
+
return (_jsxs("div", { className: "duration-legend", children: [_jsxs("span", { children: [_jsx("i", { className: "legend-blue" }), t(locale, "requestProcessing")] }), _jsxs("span", { children: [_jsx("i", { className: "legend-purple" }), t(locale, "application")] }), _jsxs("span", { children: [_jsx("i", { className: "legend-red" }), t(locale, "authDecisionLegend")] }), _jsxs("span", { children: [_jsx("i", { className: "legend-orange" }), t(locale, "result")] }), _jsxs("span", { children: [_jsx("i", { className: "legend-hatched" }), t(locale, "notProven")] })] }));
|
|
2043
|
+
}
|
|
2044
|
+
function ConfidenceBars() {
|
|
2045
|
+
return (_jsxs("span", { className: "confidence-bars", "aria-hidden": "true", children: [_jsx("i", {}), _jsx("i", {}), _jsx("i", {})] }));
|
|
2046
|
+
}
|
|
2047
|
+
function parseFlowRecord(value) {
|
|
2048
|
+
try {
|
|
2049
|
+
const parsed = JSON.parse(value);
|
|
2050
|
+
if (typeof parsed.id === "string" &&
|
|
2051
|
+
typeof parsed.requestId === "string" &&
|
|
2052
|
+
typeof parsed.method === "string" &&
|
|
2053
|
+
typeof parsed.path === "string" &&
|
|
2054
|
+
(parsed.pageUrl === undefined || typeof parsed.pageUrl === "string") &&
|
|
2055
|
+
Array.isArray(parsed.layers)) {
|
|
2056
|
+
return {
|
|
2057
|
+
...parsed,
|
|
2058
|
+
trigger: parsed.trigger === "user_action" ? "user_action" : "background",
|
|
2059
|
+
priority: parsed.priority === "background" ? "background" : "primary",
|
|
2060
|
+
runtimeSource: parsed.runtimeSource === "server" || parsed.runtimeSource === "browser"
|
|
2061
|
+
? parsed.runtimeSource
|
|
2062
|
+
: "unknown"
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
catch {
|
|
2067
|
+
return null;
|
|
2068
|
+
}
|
|
2069
|
+
return null;
|
|
2070
|
+
}
|
|
2071
|
+
function parsePageContextEvent(value) {
|
|
2072
|
+
try {
|
|
2073
|
+
const parsed = JSON.parse(value);
|
|
2074
|
+
if (parsed.type === "page_context" &&
|
|
2075
|
+
typeof parsed.pageUrl === "string" &&
|
|
2076
|
+
typeof parsed.contextId === "string" &&
|
|
2077
|
+
typeof parsed.observedAt === "string") {
|
|
2078
|
+
return {
|
|
2079
|
+
type: "page_context",
|
|
2080
|
+
pageUrl: parsed.pageUrl,
|
|
2081
|
+
contextId: parsed.contextId,
|
|
2082
|
+
observedAt: parsed.observedAt
|
|
2083
|
+
};
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
catch {
|
|
2087
|
+
return null;
|
|
2088
|
+
}
|
|
2089
|
+
return null;
|
|
2090
|
+
}
|
|
2091
|
+
function recordsForPageContext(records, pageContext) {
|
|
2092
|
+
if (!pageContext) {
|
|
2093
|
+
return records;
|
|
2094
|
+
}
|
|
2095
|
+
return records.filter((record) => recordBelongsToPageContext(record, pageContext));
|
|
2096
|
+
}
|
|
2097
|
+
function recordBelongsToPageContext(record, pageContext) {
|
|
2098
|
+
if (record.contextId && record.contextId === pageContext.contextId) {
|
|
2099
|
+
return true;
|
|
2100
|
+
}
|
|
2101
|
+
if (record.pageUrl) {
|
|
2102
|
+
return normalizeDisplayUrl(record.pageUrl) === normalizeDisplayUrl(pageContext.pageUrl);
|
|
2103
|
+
}
|
|
2104
|
+
return false;
|
|
2105
|
+
}
|
|
2106
|
+
function scopedRecentRecords(records, selectedRecord) {
|
|
2107
|
+
if (!selectedRecord) {
|
|
2108
|
+
return records;
|
|
2109
|
+
}
|
|
2110
|
+
const selectedAt = recordTime(selectedRecord);
|
|
2111
|
+
const windowStart = selectedAt - 1_500;
|
|
2112
|
+
const windowEnd = Math.max(Date.now(), selectedAt + 12_000);
|
|
2113
|
+
const selectedContext = selectedRecord.contextId;
|
|
2114
|
+
const scoped = records.filter((record) => {
|
|
2115
|
+
if (record.id === selectedRecord.id) {
|
|
2116
|
+
return true;
|
|
2117
|
+
}
|
|
2118
|
+
const time = recordTime(record);
|
|
2119
|
+
if (time < windowStart || time > windowEnd) {
|
|
2120
|
+
return false;
|
|
2121
|
+
}
|
|
2122
|
+
if (!selectedContext || !record.contextId) {
|
|
2123
|
+
return true;
|
|
2124
|
+
}
|
|
2125
|
+
return record.contextId === selectedContext;
|
|
2126
|
+
});
|
|
2127
|
+
return scoped.length > 0 ? scoped : [selectedRecord];
|
|
2128
|
+
}
|
|
2129
|
+
function mergeRecentRecords(records, selectedId) {
|
|
2130
|
+
const grouped = new Map();
|
|
2131
|
+
for (const record of records) {
|
|
2132
|
+
const key = [
|
|
2133
|
+
record.contextId ?? "",
|
|
2134
|
+
record.priority,
|
|
2135
|
+
record.method,
|
|
2136
|
+
normalizeComparablePath(record.path),
|
|
2137
|
+
record.status ?? "",
|
|
2138
|
+
record.matchState
|
|
2139
|
+
].join("|");
|
|
2140
|
+
grouped.set(key, [...(grouped.get(key) ?? []), record]);
|
|
2141
|
+
}
|
|
2142
|
+
return [...grouped.values()]
|
|
2143
|
+
.map((group) => {
|
|
2144
|
+
const selectedRecord = selectedId
|
|
2145
|
+
? group.find((record) => record.id === selectedId)
|
|
2146
|
+
: undefined;
|
|
2147
|
+
const latestRecord = group.reduce((latest, record) => recordTime(record) > recordTime(latest) ? record : latest);
|
|
2148
|
+
return {
|
|
2149
|
+
...(selectedRecord ?? latestRecord),
|
|
2150
|
+
occurrenceCount: group.length
|
|
2151
|
+
};
|
|
2152
|
+
})
|
|
2153
|
+
.sort((left, right) => recordTime(right) - recordTime(left));
|
|
2154
|
+
}
|
|
2155
|
+
function normalizeComparablePath(path) {
|
|
2156
|
+
try {
|
|
2157
|
+
const parsed = new URL(path, "http://anlyx.local");
|
|
2158
|
+
parsed.searchParams.sort();
|
|
2159
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
2160
|
+
}
|
|
2161
|
+
catch {
|
|
2162
|
+
return path;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
function displayPageUrl(record, locale, pageContext) {
|
|
2166
|
+
if (pageContext?.pageUrl && (!record || isPageContextCurrentForRecord(pageContext, record))) {
|
|
2167
|
+
return normalizeDisplayUrl(pageContext.pageUrl);
|
|
2168
|
+
}
|
|
2169
|
+
if (record?.pageUrl) {
|
|
2170
|
+
return normalizeDisplayUrl(record.pageUrl);
|
|
2171
|
+
}
|
|
2172
|
+
if (record?.contextId?.startsWith("page:")) {
|
|
2173
|
+
const path = record.contextId.slice("page:".length) || "/";
|
|
2174
|
+
return normalizeDisplayUrl(path);
|
|
2175
|
+
}
|
|
2176
|
+
return t(locale, "pageUrlNotCaptured");
|
|
2177
|
+
}
|
|
2178
|
+
function isPageContextCurrentForRecord(pageContext, record) {
|
|
2179
|
+
if (!record.pageUrl && !record.contextId) {
|
|
2180
|
+
return true;
|
|
2181
|
+
}
|
|
2182
|
+
const pageObservedAt = Date.parse(pageContext.observedAt);
|
|
2183
|
+
const recordCreatedAt = Date.parse(record.createdAt);
|
|
2184
|
+
if (!Number.isFinite(pageObservedAt) || !Number.isFinite(recordCreatedAt)) {
|
|
2185
|
+
return pageContext.contextId !== record.contextId;
|
|
2186
|
+
}
|
|
2187
|
+
return pageObservedAt >= recordCreatedAt || pageContext.contextId !== record.contextId;
|
|
2188
|
+
}
|
|
2189
|
+
function normalizeDisplayUrl(urlOrPath) {
|
|
2190
|
+
try {
|
|
2191
|
+
const parsed = new URL(urlOrPath);
|
|
2192
|
+
if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
|
|
2193
|
+
return `${parsed.pathname}${parsed.search}` || "/";
|
|
2194
|
+
}
|
|
2195
|
+
return `${parsed.origin}${parsed.pathname}${parsed.search}`;
|
|
2196
|
+
}
|
|
2197
|
+
catch {
|
|
2198
|
+
return urlOrPath || "/";
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
function isPrimaryRecord(record) {
|
|
2202
|
+
if (record.priority === "background") {
|
|
2203
|
+
return false;
|
|
2204
|
+
}
|
|
2205
|
+
if (record.trigger === "user_action") {
|
|
2206
|
+
return true;
|
|
2207
|
+
}
|
|
2208
|
+
return !isPassiveImplementationRequest(record);
|
|
2209
|
+
}
|
|
2210
|
+
function isPassiveImplementationRequest(record) {
|
|
2211
|
+
const method = record.method.toUpperCase();
|
|
2212
|
+
const segments = record.path.split("?")[0]?.toLowerCase().split("/").filter(Boolean) ?? [];
|
|
2213
|
+
const last = segments[segments.length - 1] ?? "";
|
|
2214
|
+
if (segments.some((segment) => [
|
|
2215
|
+
"health",
|
|
2216
|
+
"healthz",
|
|
2217
|
+
"ready",
|
|
2218
|
+
"readyz",
|
|
2219
|
+
"live",
|
|
2220
|
+
"livez",
|
|
2221
|
+
"ping",
|
|
2222
|
+
"metrics",
|
|
2223
|
+
"poll",
|
|
2224
|
+
"polling"
|
|
2225
|
+
].includes(segment))) {
|
|
2226
|
+
return true;
|
|
2227
|
+
}
|
|
2228
|
+
if (segments.some((segment) => ["page-views", "analytics", "telemetry", "events", "metrics"].includes(segment))) {
|
|
2229
|
+
return true;
|
|
2230
|
+
}
|
|
2231
|
+
if (method === "GET" && ["me", "session", "profile", "current-user"].includes(last)) {
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
if (segments.includes("csrf") || segments.includes("xsrf")) {
|
|
2235
|
+
return true;
|
|
2236
|
+
}
|
|
2237
|
+
if (segments.includes("auth") &&
|
|
2238
|
+
["session", "refresh", "token", "csrf", "status"].includes(last)) {
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
return (method === "GET" &&
|
|
2242
|
+
["saved-benefits", "saved-items", "bookmarks", "favorites"].some((segment) => segments.includes(segment)));
|
|
2243
|
+
}
|
|
2244
|
+
function recordTime(record) {
|
|
2245
|
+
const time = Date.parse(record.createdAt);
|
|
2246
|
+
return Number.isFinite(time) ? time : Date.now();
|
|
2247
|
+
}
|
|
2248
|
+
function orderedLayers(record) {
|
|
2249
|
+
const used = new Set();
|
|
2250
|
+
const ordered = [];
|
|
2251
|
+
for (const type of layerOrder) {
|
|
2252
|
+
for (const layer of record.layers) {
|
|
2253
|
+
if (layer.type === type && !used.has(layer.id)) {
|
|
2254
|
+
used.add(layer.id);
|
|
2255
|
+
ordered.push(layer);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
}
|
|
2259
|
+
for (const layer of record.layers) {
|
|
2260
|
+
if (!used.has(layer.id)) {
|
|
2261
|
+
ordered.push(layer);
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return ordered;
|
|
2265
|
+
}
|
|
2266
|
+
function timingLayers(record) {
|
|
2267
|
+
if (!record.backendSpans?.length) {
|
|
2268
|
+
return assignReadableOffsets(orderedLayers(record).filter((layer) => layer.type !== "page"), Math.max(record.durationMs ?? record.duration ?? 1, 1));
|
|
2269
|
+
}
|
|
2270
|
+
const action = record.layers.find((layer) => layer.type === "action");
|
|
2271
|
+
const api = record.layers.find((layer) => layer.type === "api");
|
|
2272
|
+
const result = record.layers.find((layer) => layer.type === "result");
|
|
2273
|
+
const total = Math.max(record.durationMs ?? record.duration ?? 1, 1);
|
|
2274
|
+
const layers = [];
|
|
2275
|
+
if (action) {
|
|
2276
|
+
layers.push({
|
|
2277
|
+
...action,
|
|
2278
|
+
startOffsetMs: 0,
|
|
2279
|
+
durationMs: Math.min(action.durationMs ?? 4, 4)
|
|
2280
|
+
});
|
|
2281
|
+
}
|
|
2282
|
+
if (api) {
|
|
2283
|
+
layers.push({
|
|
2284
|
+
...api,
|
|
2285
|
+
startOffsetMs: 3,
|
|
2286
|
+
durationMs: Math.max(1, (record.durationMs ?? record.duration ?? api.durationMs ?? 1) - 3)
|
|
2287
|
+
});
|
|
2288
|
+
}
|
|
2289
|
+
layers.push(...[...record.backendSpans]
|
|
2290
|
+
.filter((span) => span.type !== "api")
|
|
2291
|
+
.sort(compareBackendSpans)
|
|
2292
|
+
.map(backendSpanToLayer));
|
|
2293
|
+
if (result) {
|
|
2294
|
+
layers.push({
|
|
2295
|
+
...result,
|
|
2296
|
+
startOffsetMs: Math.max(0, total - Math.max(1, result.durationMs ?? 1)),
|
|
2297
|
+
durationMs: Math.max(1, result.durationMs ?? 1)
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
return layers;
|
|
2301
|
+
}
|
|
2302
|
+
function compactTimingLayers(record) {
|
|
2303
|
+
if (!record.backendSpans?.length) {
|
|
2304
|
+
return timingLayers(record);
|
|
2305
|
+
}
|
|
2306
|
+
const rawRows = timingLayers(record);
|
|
2307
|
+
const action = rawRows.find((layer) => layer.type === "action");
|
|
2308
|
+
const api = rawRows.find((layer) => layer.type === "api");
|
|
2309
|
+
const result = rawRows.find((layer) => layer.type === "result");
|
|
2310
|
+
const observedRows = summarizeBackendObservedSpans(record.backendSpans);
|
|
2311
|
+
const requestOverhead = requestOverheadLayer(record, api, result);
|
|
2312
|
+
const sourceRows = orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
2313
|
+
const observedTypes = new Set(observedRows.map((layer) => layer.type));
|
|
2314
|
+
const unprovenRows = sourceRows.filter((layer) => isUnprovenLayer(layer) &&
|
|
2315
|
+
!observedTypes.has(layer.type) &&
|
|
2316
|
+
layer.type !== "action" &&
|
|
2317
|
+
layer.type !== "api" &&
|
|
2318
|
+
layer.type !== "result");
|
|
2319
|
+
return [action, requestOverhead, ...observedRows, result, ...unprovenRows].filter((layer) => Boolean(layer));
|
|
2320
|
+
}
|
|
2321
|
+
function summaryRows(record) {
|
|
2322
|
+
if (!record.backendSpans?.length) {
|
|
2323
|
+
return orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
2324
|
+
}
|
|
2325
|
+
const sourceRows = orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
2326
|
+
const action = sourceRows.find((layer) => layer.type === "action");
|
|
2327
|
+
const api = sourceRows.find((layer) => layer.type === "api");
|
|
2328
|
+
const result = sourceRows.find((layer) => layer.type === "result");
|
|
2329
|
+
const observedRows = summarizeBackendObservedSpans(record.backendSpans);
|
|
2330
|
+
const requestOverhead = requestOverheadLayer(record, api, result);
|
|
2331
|
+
const observedTypes = new Set(observedRows.map((layer) => layer.type));
|
|
2332
|
+
const unprovenRows = sourceRows.filter((layer) => isUnprovenLayer(layer) &&
|
|
2333
|
+
!observedTypes.has(layer.type) &&
|
|
2334
|
+
layer.type !== "action" &&
|
|
2335
|
+
layer.type !== "api" &&
|
|
2336
|
+
layer.type !== "result");
|
|
2337
|
+
return [action, requestOverhead, ...observedRows, result, ...unprovenRows].filter((layer) => Boolean(layer));
|
|
2338
|
+
}
|
|
2339
|
+
function assignReadableOffsets(layers, total) {
|
|
2340
|
+
const backend = layers.filter((layer) => layer.type !== "action" &&
|
|
2341
|
+
layer.type !== "api" &&
|
|
2342
|
+
layer.type !== "result" &&
|
|
2343
|
+
!isUnprovenLayer(layer));
|
|
2344
|
+
const backendStart = Math.max(14, Math.round(total * 0.08));
|
|
2345
|
+
const backendEnd = Math.max(backendStart + 1, Math.round(total * 0.78));
|
|
2346
|
+
const slot = backend.length > 0 ? Math.max(8, Math.round((backendEnd - backendStart) / backend.length)) : 0;
|
|
2347
|
+
return layers.map((layer) => {
|
|
2348
|
+
if (layer.startOffsetMs !== undefined)
|
|
2349
|
+
return layer;
|
|
2350
|
+
if (layer.type === "action") {
|
|
2351
|
+
return { ...layer, startOffsetMs: 0, durationMs: Math.min(layer.durationMs ?? 4, 4) };
|
|
2352
|
+
}
|
|
2353
|
+
if (layer.type === "api") {
|
|
2354
|
+
return {
|
|
2355
|
+
...layer,
|
|
2356
|
+
startOffsetMs: 3,
|
|
2357
|
+
durationMs: Math.max(1, total - 3)
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
if (layer.type === "result") {
|
|
2361
|
+
return {
|
|
2362
|
+
...layer,
|
|
2363
|
+
startOffsetMs: Math.max(0, total - Math.max(1, layer.durationMs ?? 2)),
|
|
2364
|
+
durationMs: Math.max(1, layer.durationMs ?? 2)
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
if (isUnprovenLayer(layer)) {
|
|
2368
|
+
return layer;
|
|
2369
|
+
}
|
|
2370
|
+
const index = Math.max(0, backend.findIndex((item) => item.id === layer.id));
|
|
2371
|
+
const estimatedDuration = layer.durationMs ?? estimatedLayerDuration(layer, total);
|
|
2372
|
+
return {
|
|
2373
|
+
...layer,
|
|
2374
|
+
startOffsetMs: Math.min(Math.max(0, total - estimatedDuration), backendStart + slot * index),
|
|
2375
|
+
durationMs: estimatedDuration
|
|
2376
|
+
};
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
function backendSpanToLayer(span) {
|
|
2380
|
+
return {
|
|
2381
|
+
id: `backend:${span.id}`,
|
|
2382
|
+
type: span.type,
|
|
2383
|
+
label: span.label,
|
|
2384
|
+
execution: "observed",
|
|
2385
|
+
evidenceLevel: "backend_observed",
|
|
2386
|
+
evidence: span.evidence ?? ["backend_observed: server-side method span reported by dev bridge"],
|
|
2387
|
+
...(span.nodeId ? { nodeId: span.nodeId } : {}),
|
|
2388
|
+
...(span.filePath ? { filePath: span.filePath } : {}),
|
|
2389
|
+
...(span.lineNumber !== undefined ? { lineNumber: span.lineNumber } : {}),
|
|
2390
|
+
startOffsetMs: span.startOffsetMs,
|
|
2391
|
+
durationMs: span.durationMs,
|
|
2392
|
+
...(span.status !== undefined ? { status: span.status } : {})
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
function diagramLayers(record) {
|
|
2396
|
+
if (!record.backendSpans?.length) {
|
|
2397
|
+
return orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
2398
|
+
}
|
|
2399
|
+
return summaryRows(record).filter((layer) => !isUnprovenLayer(layer));
|
|
2400
|
+
}
|
|
2401
|
+
function requestOverheadLayer(record, api, result) {
|
|
2402
|
+
if (!api || !record.backendSpans?.length) {
|
|
2403
|
+
return api;
|
|
2404
|
+
}
|
|
2405
|
+
const backendSpans = record.backendSpans.filter((span) => span.type !== "api");
|
|
2406
|
+
const total = Math.max(1, record.durationMs ?? record.duration ?? api.durationMs ?? 1);
|
|
2407
|
+
const backendCoveredDuration = coveredDurationMs(backendSpans);
|
|
2408
|
+
const resultDuration = Math.max(0, result?.durationMs ?? 0);
|
|
2409
|
+
const durationMs = Math.max(0, total - backendCoveredDuration - resultDuration);
|
|
2410
|
+
return {
|
|
2411
|
+
...api,
|
|
2412
|
+
label: `${record.method} ${record.path}`,
|
|
2413
|
+
startOffsetMs: 0,
|
|
2414
|
+
durationMs
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
function summarizeBackendObservedSpans(spans) {
|
|
2418
|
+
const groupedSpans = new Map();
|
|
2419
|
+
const databaseSpans = [];
|
|
2420
|
+
const untrackedControllerSpans = [];
|
|
2421
|
+
const selfDurations = backendSelfDurationBySpanId(spans);
|
|
2422
|
+
for (const span of [...spans].sort(compareBackendSpans)) {
|
|
2423
|
+
if (span.type === "api") {
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
if (span.type === "controller") {
|
|
2427
|
+
untrackedControllerSpans.push(span);
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (span.type === "database") {
|
|
2431
|
+
databaseSpans.push(span);
|
|
2432
|
+
continue;
|
|
2433
|
+
}
|
|
2434
|
+
const key = `${span.type}:${span.label}`;
|
|
2435
|
+
groupedSpans.set(key, [...(groupedSpans.get(key) ?? []), span]);
|
|
2436
|
+
}
|
|
2437
|
+
const rows = [...groupedSpans.values()].map((group) => groupedBackendSpansToLayer(group, selfDurations));
|
|
2438
|
+
if (databaseSpans.length > 0) {
|
|
2439
|
+
rows.push(databaseSpansToSummaryLayer(databaseSpans, selfDurations));
|
|
2440
|
+
}
|
|
2441
|
+
if (untrackedControllerSpans.length > 0) {
|
|
2442
|
+
rows.push(untrackedControllerSpansToLayer(untrackedControllerSpans, selfDurations));
|
|
2443
|
+
}
|
|
2444
|
+
return rows.sort(compareFlowLayersByTiming);
|
|
2445
|
+
}
|
|
2446
|
+
function groupedBackendSpansToLayer(spans, selfDurations) {
|
|
2447
|
+
if (spans.length === 1) {
|
|
2448
|
+
const span = spans[0];
|
|
2449
|
+
return backendSpanToLayer({
|
|
2450
|
+
...span,
|
|
2451
|
+
durationMs: Math.max(0, Math.round(selfDurations.get(span.id) ?? span.durationMs))
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
const first = spans[0];
|
|
2455
|
+
const startOffsetMs = Math.min(...spans.map((span) => span.startOffsetMs));
|
|
2456
|
+
const evidence = spans.flatMap((span) => span.evidence ?? []);
|
|
2457
|
+
const selfDurationMs = spans.reduce((sum, span) => sum + Math.max(0, selfDurations.get(span.id) ?? span.durationMs), 0);
|
|
2458
|
+
return {
|
|
2459
|
+
id: `backend:group:${first.type}:${first.label}`,
|
|
2460
|
+
type: first.type,
|
|
2461
|
+
label: `${first.label} ×${spans.length}`,
|
|
2462
|
+
execution: "observed",
|
|
2463
|
+
evidenceLevel: "backend_observed",
|
|
2464
|
+
evidence: [
|
|
2465
|
+
`backend_observed: ${spans.length} runtime ${first.type} span${spans.length === 1 ? "" : "s"} grouped by label`,
|
|
2466
|
+
...evidence.slice(0, 2)
|
|
2467
|
+
],
|
|
2468
|
+
startOffsetMs,
|
|
2469
|
+
durationMs: Math.max(0, Math.round(selfDurationMs))
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
function databaseSpansToSummaryLayer(spans, selfDurations) {
|
|
2473
|
+
const startOffsetMs = Math.min(...spans.map((span) => span.startOffsetMs));
|
|
2474
|
+
const firstEvidence = spans.flatMap((span) => span.evidence ?? [])[0];
|
|
2475
|
+
const selfDurationMs = spans.reduce((sum, span) => sum + Math.max(0, selfDurations.get(span.id) ?? span.durationMs), 0);
|
|
2476
|
+
return {
|
|
2477
|
+
id: "backend:database-summary",
|
|
2478
|
+
type: "database",
|
|
2479
|
+
label: `${spans.length} JDBC statement${spans.length === 1 ? "" : "s"}`,
|
|
2480
|
+
execution: "observed",
|
|
2481
|
+
evidenceLevel: "backend_observed",
|
|
2482
|
+
evidence: [
|
|
2483
|
+
`backend_observed: ${spans.length} JDBC execute span${spans.length === 1 ? "" : "s"} reported by dev bridge`,
|
|
2484
|
+
...(firstEvidence ? [firstEvidence] : [])
|
|
2485
|
+
],
|
|
2486
|
+
startOffsetMs,
|
|
2487
|
+
durationMs: Math.max(0, Math.round(selfDurationMs))
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
function untrackedControllerSpansToLayer(spans, selfDurations) {
|
|
2491
|
+
const startOffsetMs = Math.min(...spans.map((span) => span.startOffsetMs));
|
|
2492
|
+
const selfDurationMs = spans.reduce((sum, span) => sum + Math.max(0, selfDurations.get(span.id) ?? 0), 0);
|
|
2493
|
+
return {
|
|
2494
|
+
id: "backend:untracked-app",
|
|
2495
|
+
type: "unknown",
|
|
2496
|
+
label: "Untracked application time",
|
|
2497
|
+
execution: "observed",
|
|
2498
|
+
evidenceLevel: "backend_observed",
|
|
2499
|
+
evidence: [
|
|
2500
|
+
"backend_observed: controller runtime remained after measured child spans were removed"
|
|
2501
|
+
],
|
|
2502
|
+
startOffsetMs,
|
|
2503
|
+
durationMs: Math.max(0, Math.round(selfDurationMs))
|
|
2504
|
+
};
|
|
2505
|
+
}
|
|
2506
|
+
function backendSelfDurationBySpanId(spans) {
|
|
2507
|
+
const childrenByParent = new Map();
|
|
2508
|
+
for (const span of spans) {
|
|
2509
|
+
if (!span.parentId) {
|
|
2510
|
+
continue;
|
|
2511
|
+
}
|
|
2512
|
+
childrenByParent.set(span.parentId, [...(childrenByParent.get(span.parentId) ?? []), span]);
|
|
2513
|
+
}
|
|
2514
|
+
return new Map(spans.map((span) => {
|
|
2515
|
+
const childDuration = coveredDurationMs(childrenByParent.get(span.id) ?? []);
|
|
2516
|
+
return [span.id, Math.max(0, span.durationMs - childDuration)];
|
|
2517
|
+
}));
|
|
2518
|
+
}
|
|
2519
|
+
function coveredDurationMs(spans) {
|
|
2520
|
+
const intervals = spans
|
|
2521
|
+
.filter((span) => span.durationMs > 0)
|
|
2522
|
+
.map((span) => ({
|
|
2523
|
+
start: span.startOffsetMs,
|
|
2524
|
+
end: span.startOffsetMs + span.durationMs
|
|
2525
|
+
}))
|
|
2526
|
+
.sort((left, right) => left.start - right.start);
|
|
2527
|
+
let covered = 0;
|
|
2528
|
+
let currentStart;
|
|
2529
|
+
let currentEnd;
|
|
2530
|
+
for (const interval of intervals) {
|
|
2531
|
+
if (currentStart === undefined || currentEnd === undefined) {
|
|
2532
|
+
currentStart = interval.start;
|
|
2533
|
+
currentEnd = interval.end;
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
if (interval.start <= currentEnd) {
|
|
2537
|
+
currentEnd = Math.max(currentEnd, interval.end);
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
covered += currentEnd - currentStart;
|
|
2541
|
+
currentStart = interval.start;
|
|
2542
|
+
currentEnd = interval.end;
|
|
2543
|
+
}
|
|
2544
|
+
if (currentStart !== undefined && currentEnd !== undefined) {
|
|
2545
|
+
covered += currentEnd - currentStart;
|
|
2546
|
+
}
|
|
2547
|
+
return Math.max(0, covered);
|
|
2548
|
+
}
|
|
2549
|
+
function findFirstLayer(layers, types) {
|
|
2550
|
+
return layers.find((layer) => types.includes(layer.type));
|
|
2551
|
+
}
|
|
2552
|
+
function compareBackendSpans(a, b) {
|
|
2553
|
+
const timingDelta = a.startOffsetMs - b.startOffsetMs;
|
|
2554
|
+
if (timingDelta !== 0)
|
|
2555
|
+
return timingDelta;
|
|
2556
|
+
if (a.id === b.parentId)
|
|
2557
|
+
return -1;
|
|
2558
|
+
if (a.parentId === b.id)
|
|
2559
|
+
return 1;
|
|
2560
|
+
return layerOrderIndex(a.type) - layerOrderIndex(b.type) || a.label.localeCompare(b.label);
|
|
2561
|
+
}
|
|
2562
|
+
function compareFlowLayersByTiming(a, b) {
|
|
2563
|
+
const timingDelta = (a.startOffsetMs ?? Number.MAX_SAFE_INTEGER) - (b.startOffsetMs ?? Number.MAX_SAFE_INTEGER);
|
|
2564
|
+
if (timingDelta !== 0)
|
|
2565
|
+
return timingDelta;
|
|
2566
|
+
return layerOrderIndex(a.type) - layerOrderIndex(b.type) || a.label.localeCompare(b.label);
|
|
2567
|
+
}
|
|
2568
|
+
function layerOrderIndex(type) {
|
|
2569
|
+
const index = layerOrder.indexOf(type);
|
|
2570
|
+
return index === -1 ? layerOrder.length : index;
|
|
2571
|
+
}
|
|
2572
|
+
function isUnprovenLayer(layer) {
|
|
2573
|
+
return layer.execution === "not_proven" || layer.evidenceLevel === "not_proven";
|
|
2574
|
+
}
|
|
2575
|
+
function isUntrackedAppLayer(layer) {
|
|
2576
|
+
return layer.id === "backend:untracked-app";
|
|
2577
|
+
}
|
|
2578
|
+
function isSourceMatchedLayer(layer) {
|
|
2579
|
+
return layer.execution === "scanned" || layer.evidenceLevel === "source_derived";
|
|
2580
|
+
}
|
|
2581
|
+
function isDecisionLayer(layer) {
|
|
2582
|
+
return layer.type === "auth" || layer.type === "decision";
|
|
2583
|
+
}
|
|
2584
|
+
function summaryStatusLabel(layer, locale) {
|
|
2585
|
+
if (isUnprovenLayer(layer))
|
|
2586
|
+
return t(locale, "notProven");
|
|
2587
|
+
if (layer.execution === "blocked")
|
|
2588
|
+
return t(locale, "blocked");
|
|
2589
|
+
if (layer.evidenceLevel === "browser_observed" || layer.evidenceLevel === "backend_observed") {
|
|
2590
|
+
return t(locale, "observed");
|
|
2591
|
+
}
|
|
2592
|
+
if (layer.evidenceLevel === "inferred")
|
|
2593
|
+
return t(locale, "inferred");
|
|
2594
|
+
if (isSourceMatchedLayer(layer))
|
|
2595
|
+
return t(locale, "sourceMatched");
|
|
2596
|
+
return t(locale, "matched");
|
|
2597
|
+
}
|
|
2598
|
+
function summaryStatusTone(layer) {
|
|
2599
|
+
if (isUnprovenLayer(layer) || isSourceMatchedLayer(layer))
|
|
2600
|
+
return "scanned";
|
|
2601
|
+
if (layer.execution === "blocked")
|
|
2602
|
+
return "blocked";
|
|
2603
|
+
if (layer.evidenceLevel === "inferred")
|
|
2604
|
+
return "inferred";
|
|
2605
|
+
return "matched";
|
|
2606
|
+
}
|
|
2607
|
+
function visualLayerType(layer) {
|
|
2608
|
+
if (isUntrackedAppLayer(layer))
|
|
2609
|
+
return "untracked";
|
|
2610
|
+
if (layer.type === "decision")
|
|
2611
|
+
return "auth";
|
|
2612
|
+
if (layer.type === "action" ||
|
|
2613
|
+
layer.type === "api" ||
|
|
2614
|
+
layer.type === "controller" ||
|
|
2615
|
+
layer.type === "auth" ||
|
|
2616
|
+
layer.type === "result" ||
|
|
2617
|
+
layer.type === "service" ||
|
|
2618
|
+
layer.type === "repository" ||
|
|
2619
|
+
layer.type === "database") {
|
|
2620
|
+
return layer.type;
|
|
2621
|
+
}
|
|
2622
|
+
return "controller";
|
|
2623
|
+
}
|
|
2624
|
+
function layerLabel(layer, locale = "en") {
|
|
2625
|
+
if (isUntrackedAppLayer(layer))
|
|
2626
|
+
return t(locale, "untrackedApp");
|
|
2627
|
+
if (layer.type === "api")
|
|
2628
|
+
return "API";
|
|
2629
|
+
if (layer.type === "auth")
|
|
2630
|
+
return locale === "ko" ? "인증 / 세션" : "Auth / Session";
|
|
2631
|
+
if (layer.type === "action")
|
|
2632
|
+
return locale === "ko" ? "사용자 액션" : "Action";
|
|
2633
|
+
if (layer.type === "controller")
|
|
2634
|
+
return locale === "ko" ? "컨트롤러" : "Controller";
|
|
2635
|
+
if (layer.type === "decision")
|
|
2636
|
+
return locale === "ko" ? "결정" : "Decision";
|
|
2637
|
+
if (layer.type === "service")
|
|
2638
|
+
return t(locale, "service");
|
|
2639
|
+
if (layer.type === "repository")
|
|
2640
|
+
return t(locale, "repository");
|
|
2641
|
+
if (layer.type === "database")
|
|
2642
|
+
return t(locale, "database");
|
|
2643
|
+
if (layer.type === "result")
|
|
2644
|
+
return t(locale, "result");
|
|
2645
|
+
return capitalize(layer.type);
|
|
2646
|
+
}
|
|
2647
|
+
function estimatedLayerDuration(layer, total) {
|
|
2648
|
+
if (layer.durationMs !== undefined) {
|
|
2649
|
+
return layer.durationMs;
|
|
2650
|
+
}
|
|
2651
|
+
if (isUnprovenLayer(layer)) {
|
|
2652
|
+
return Math.max(1, Math.round(total * 0.06));
|
|
2653
|
+
}
|
|
2654
|
+
if (layer.type === "result") {
|
|
2655
|
+
return 1;
|
|
2656
|
+
}
|
|
2657
|
+
return Math.max(1, Math.round(total * 0.18));
|
|
2658
|
+
}
|
|
2659
|
+
function layerOffset(layer, total) {
|
|
2660
|
+
if (layer.startOffsetMs !== undefined) {
|
|
2661
|
+
return Math.min(96, Math.max(0, (layer.startOffsetMs / Math.max(total, 1)) * 100));
|
|
2662
|
+
}
|
|
2663
|
+
if (layer.type === "action" || layer.type === "api")
|
|
2664
|
+
return 0;
|
|
2665
|
+
if (layer.type === "controller")
|
|
2666
|
+
return 2;
|
|
2667
|
+
if (isDecisionLayer(layer))
|
|
2668
|
+
return 10;
|
|
2669
|
+
if (layer.type === "result")
|
|
2670
|
+
return 76;
|
|
2671
|
+
return 9;
|
|
2672
|
+
}
|
|
2673
|
+
function timelineTicks(total) {
|
|
2674
|
+
const steps = 4;
|
|
2675
|
+
return Array.from({ length: steps + 1 }, (_, index) => (total / steps) * index);
|
|
2676
|
+
}
|
|
2677
|
+
function timingBreakdownSegments(rows, total) {
|
|
2678
|
+
const types = [
|
|
2679
|
+
"api",
|
|
2680
|
+
"controller",
|
|
2681
|
+
"service",
|
|
2682
|
+
"repository",
|
|
2683
|
+
"database",
|
|
2684
|
+
"untracked",
|
|
2685
|
+
"result"
|
|
2686
|
+
];
|
|
2687
|
+
const labels = {
|
|
2688
|
+
api: "API",
|
|
2689
|
+
controller: "Controller",
|
|
2690
|
+
service: "Service",
|
|
2691
|
+
repository: "Repository",
|
|
2692
|
+
database: "Database",
|
|
2693
|
+
untracked: "Untracked",
|
|
2694
|
+
result: "Result"
|
|
2695
|
+
};
|
|
2696
|
+
const durations = new Map(types.map((type) => [type, 0]));
|
|
2697
|
+
for (const row of rows) {
|
|
2698
|
+
if (isUnprovenLayer(row)) {
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
const type = isUntrackedAppLayer(row) ? "untracked" : row.type;
|
|
2702
|
+
if (!types.includes(type)) {
|
|
2703
|
+
continue;
|
|
2704
|
+
}
|
|
2705
|
+
durations.set(type, (durations.get(type) ?? 0) + Math.max(0, row.durationMs ?? 0));
|
|
2706
|
+
}
|
|
2707
|
+
const rawNonApiDuration = types
|
|
2708
|
+
.filter((type) => type !== "api")
|
|
2709
|
+
.reduce((sum, type) => sum + (durations.get(type) ?? 0), 0);
|
|
2710
|
+
const nonApiScale = rawNonApiDuration > total ? total / rawNonApiDuration : 1;
|
|
2711
|
+
if (nonApiScale < 1) {
|
|
2712
|
+
for (const type of types) {
|
|
2713
|
+
if (type !== "api") {
|
|
2714
|
+
durations.set(type, (durations.get(type) ?? 0) * nonApiScale);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
const nonApiDuration = types
|
|
2719
|
+
.filter((type) => type !== "api")
|
|
2720
|
+
.reduce((sum, type) => sum + (durations.get(type) ?? 0), 0);
|
|
2721
|
+
durations.set("api", Math.max(0, total - nonApiDuration));
|
|
2722
|
+
const measuredTotal = Math.max(total, 1);
|
|
2723
|
+
return types.map((type) => {
|
|
2724
|
+
const durationMs = durations.get(type) ?? 0;
|
|
2725
|
+
return {
|
|
2726
|
+
durationMs,
|
|
2727
|
+
label: labels[type],
|
|
2728
|
+
type,
|
|
2729
|
+
width: durationMs <= 0 ? 0 : (durationMs / measuredTotal) * 100
|
|
2730
|
+
};
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
function timelineX(percent) {
|
|
2734
|
+
const clamped = Math.min(100, Math.max(0, percent));
|
|
2735
|
+
return TIMELINE_START_INSET_PERCENT + (clamped / 100) * (100 - TIMELINE_START_INSET_PERCENT);
|
|
2736
|
+
}
|
|
2737
|
+
function timelineWidth(left, width) {
|
|
2738
|
+
const visualLeft = timelineX(left);
|
|
2739
|
+
const visualRight = timelineX(Math.min(100, left + width));
|
|
2740
|
+
return Math.max(0, visualRight - visualLeft);
|
|
2741
|
+
}
|
|
2742
|
+
function hasBlockedOutcome(record) {
|
|
2743
|
+
return record.status !== undefined && [401, 403, 409].includes(record.status);
|
|
2744
|
+
}
|
|
2745
|
+
function OutcomeIcon({ record, size }) {
|
|
2746
|
+
if (hasBlockedOutcome(record)) {
|
|
2747
|
+
return _jsx(Lock, { size: size });
|
|
2748
|
+
}
|
|
2749
|
+
if (record.status !== undefined && record.status >= 500) {
|
|
2750
|
+
return _jsx(Flag, { size: size });
|
|
2751
|
+
}
|
|
2752
|
+
return _jsx(Check, { size: size });
|
|
2753
|
+
}
|
|
2754
|
+
function outcomeLabel(record, locale) {
|
|
2755
|
+
if (record.status === undefined)
|
|
2756
|
+
return t(locale, "pending");
|
|
2757
|
+
if (record.status >= 200 && record.status < 300)
|
|
2758
|
+
return t(locale, "ok");
|
|
2759
|
+
if (hasBlockedOutcome(record))
|
|
2760
|
+
return t(locale, "blocked");
|
|
2761
|
+
if (record.status >= 500)
|
|
2762
|
+
return t(locale, "serverError");
|
|
2763
|
+
return locale === "ko" ? "관찰됨" : "Observed";
|
|
2764
|
+
}
|
|
2765
|
+
function outcomeStatusText(record, locale) {
|
|
2766
|
+
if (record.status === undefined)
|
|
2767
|
+
return t(locale, "pending");
|
|
2768
|
+
if (record.status >= 200 && record.status < 300)
|
|
2769
|
+
return t(locale, "ok");
|
|
2770
|
+
if (record.status === 401)
|
|
2771
|
+
return t(locale, "unauthorized");
|
|
2772
|
+
if (record.status === 403)
|
|
2773
|
+
return t(locale, "forbidden");
|
|
2774
|
+
if (record.status === 409)
|
|
2775
|
+
return t(locale, "conflict");
|
|
2776
|
+
if (record.status >= 500)
|
|
2777
|
+
return t(locale, "serverError");
|
|
2778
|
+
return locale === "ko" ? "관찰됨" : "Observed";
|
|
2779
|
+
}
|
|
2780
|
+
function outcomeTone(record) {
|
|
2781
|
+
if (record.status !== undefined && record.status >= 200 && record.status < 300)
|
|
2782
|
+
return "success";
|
|
2783
|
+
if (hasBlockedOutcome(record))
|
|
2784
|
+
return "blocked";
|
|
2785
|
+
return "error";
|
|
2786
|
+
}
|
|
2787
|
+
function outcomeDescription(record, locale) {
|
|
2788
|
+
if (record.status !== undefined && record.status >= 200 && record.status < 300) {
|
|
2789
|
+
if (record.matchState !== "matched") {
|
|
2790
|
+
if (record.backendSpans?.length) {
|
|
2791
|
+
return locale === "ko"
|
|
2792
|
+
? "요청이 완료되었고 개발 런타임 span이 관찰되었습니다. 가져온 Flow JSON과는 아직 매칭되지 않았습니다."
|
|
2793
|
+
: "Request completed with development runtime spans observed. It is not matched to imported Flow JSON yet.";
|
|
2794
|
+
}
|
|
2795
|
+
return locale === "ko"
|
|
2796
|
+
? "요청이 완료되었지만 가져온 Flow JSON과는 아직 매칭되지 않았습니다."
|
|
2797
|
+
: "Request completed, but it is not matched to imported Flow JSON yet.";
|
|
2798
|
+
}
|
|
2799
|
+
return locale === "ko"
|
|
2800
|
+
? "요청이 완료되었고 가져온 백엔드 흐름과 매칭되었습니다."
|
|
2801
|
+
: "Request completed and matched imported backend flow.";
|
|
2802
|
+
}
|
|
2803
|
+
if (record.status === 409) {
|
|
2804
|
+
return locale === "ko"
|
|
2805
|
+
? "요청이 결정 분기에 도달했고 차단 결과를 반환했습니다."
|
|
2806
|
+
: "Request reached a decision branch and returned a blocked result.";
|
|
2807
|
+
}
|
|
2808
|
+
if (hasBlockedOutcome(record)) {
|
|
2809
|
+
return locale === "ko"
|
|
2810
|
+
? "하위 비즈니스 로직의 실행이 확인되기 전에 요청이 차단되었습니다."
|
|
2811
|
+
: "Request blocked before downstream business logic was proven executed.";
|
|
2812
|
+
}
|
|
2813
|
+
return locale === "ko"
|
|
2814
|
+
? "브라우저에서 관찰된 요청과 소스 기반 백엔드 매칭입니다. 서버 런타임 트레이스는 아닙니다."
|
|
2815
|
+
: "Browser-observed request with source-derived backend matching. This is not a server runtime trace.";
|
|
2816
|
+
}
|
|
2817
|
+
function requestContextDescription(record, locale) {
|
|
2818
|
+
const hasRuntimeSpans = Boolean(record.backendSpans?.length);
|
|
2819
|
+
if (record.matchState === "matched" && hasRuntimeSpans) {
|
|
2820
|
+
return t(locale, "mappedEvidenceWithRuntime");
|
|
2821
|
+
}
|
|
2822
|
+
if (record.matchState === "matched") {
|
|
2823
|
+
return t(locale, "mappedEvidence");
|
|
2824
|
+
}
|
|
2825
|
+
if (hasRuntimeSpans) {
|
|
2826
|
+
return locale === "ko"
|
|
2827
|
+
? "브라우저에서 관찰된 요청에 개발 런타임 span이 연결되었습니다. 가져온 Flow JSON 매칭은 아직 없습니다."
|
|
2828
|
+
: "Browser-observed request with development runtime spans. No imported Flow JSON match yet.";
|
|
2829
|
+
}
|
|
2830
|
+
return locale === "ko"
|
|
2831
|
+
? "브라우저에서 관찰된 요청입니다. 가져온 Flow JSON 매칭은 아직 없습니다."
|
|
2832
|
+
: "Browser-observed request. No imported Flow JSON match yet.";
|
|
2833
|
+
}
|
|
2834
|
+
function flowNote(record, locale) {
|
|
2835
|
+
if (record.backendSpans?.length) {
|
|
2836
|
+
return locale === "ko"
|
|
2837
|
+
? "관찰됨으로 표시된 백엔드 행은 이 요청에 대한 개발 전용 백엔드 브리지에서 왔습니다. 소스 행은 가져온 근거를 기반으로 합니다."
|
|
2838
|
+
: "Backend rows marked observed came from a development-only backend bridge for this request. Source rows still come from imported evidence.";
|
|
2839
|
+
}
|
|
2840
|
+
const hasUnprovenDownstream = record.layers.some((layer) => (layer.type === "service" || layer.type === "repository" || layer.type === "database") &&
|
|
2841
|
+
isUnprovenLayer(layer));
|
|
2842
|
+
if (hasUnprovenDownstream) {
|
|
2843
|
+
return locale === "ko"
|
|
2844
|
+
? "흐리게 표시된 레이어는 소스 근거로만 알려진 경로이며 실제 실행은 확인되지 않았습니다."
|
|
2845
|
+
: "Muted layers are known by source evidence only and were not proven executed.";
|
|
2846
|
+
}
|
|
2847
|
+
return locale === "ko"
|
|
2848
|
+
? "브라우저 행은 페이지에서 관찰된 내용입니다. 소스 행은 가져온 백엔드 근거이며 운영 런타임 트레이스가 아닙니다."
|
|
2849
|
+
: "Browser rows are observed in the page. Source rows come from imported backend evidence and are not a production runtime trace.";
|
|
2850
|
+
}
|
|
2851
|
+
function confidenceDescription(record, locale) {
|
|
2852
|
+
if (record.confidence === "high") {
|
|
2853
|
+
return locale === "ko"
|
|
2854
|
+
? "브라우저 요청, 엔드포인트, 컨트롤러, 소스 근거가 모두 매칭되었습니다."
|
|
2855
|
+
: "Browser request, endpoint, controller, and source evidence all matched.";
|
|
2856
|
+
}
|
|
2857
|
+
if (record.confidence === "medium") {
|
|
2858
|
+
return locale === "ko"
|
|
2859
|
+
? "브라우저 요청은 소스 근거와 매칭되었지만 일부 백엔드 세부 정보는 추론입니다."
|
|
2860
|
+
: "The browser request matched source evidence, but some backend details are inferred.";
|
|
2861
|
+
}
|
|
2862
|
+
return locale === "ko"
|
|
2863
|
+
? "Anlyx가 브라우저 요청을 관찰했지만 백엔드 소스 근거가 충분하지 않습니다."
|
|
2864
|
+
: "Anlyx observed the browser request, but backend source evidence is incomplete.";
|
|
2865
|
+
}
|
|
2866
|
+
function evidenceCoverage(record) {
|
|
2867
|
+
return record.layers.reduce((coverage, layer) => {
|
|
2868
|
+
if (layer.evidenceLevel === "browser_observed") {
|
|
2869
|
+
coverage.browser += 1;
|
|
2870
|
+
}
|
|
2871
|
+
else if (layer.evidenceLevel === "backend_observed") {
|
|
2872
|
+
coverage.backend += 1;
|
|
2873
|
+
}
|
|
2874
|
+
else if (isUnprovenLayer(layer)) {
|
|
2875
|
+
coverage.notProven += 1;
|
|
2876
|
+
}
|
|
2877
|
+
else {
|
|
2878
|
+
coverage.source += 1;
|
|
2879
|
+
}
|
|
2880
|
+
return coverage;
|
|
2881
|
+
}, { browser: 0, backend: record.backendSpans?.length ?? 0, source: 0, notProven: 0 });
|
|
2882
|
+
}
|
|
2883
|
+
function layerSubtitle(layer, locale, runtimeSource = "browser") {
|
|
2884
|
+
if (layer.type === "action")
|
|
2885
|
+
return locale === "ko" ? "사용자 클릭 캡처됨" : "user click captured";
|
|
2886
|
+
if (layer.type === "api") {
|
|
2887
|
+
return runtimeSource === "server" ? t(locale, "serverRequestSpan") : t(locale, "browserSpan");
|
|
2888
|
+
}
|
|
2889
|
+
if (isUnprovenLayer(layer))
|
|
2890
|
+
return t(locale, "knownBySourceOnly");
|
|
2891
|
+
if (layer.evidenceLevel === "browser_observed")
|
|
2892
|
+
return t(locale, "browserObserved");
|
|
2893
|
+
if (layer.evidenceLevel === "backend_observed")
|
|
2894
|
+
return t(locale, "devRuntimeSpan");
|
|
2895
|
+
if (isSourceMatchedLayer(layer) || layer.evidenceLevel === "inferred") {
|
|
2896
|
+
return t(locale, "sourceDerivedEstimate");
|
|
2897
|
+
}
|
|
2898
|
+
return executionLabel(layer.execution, locale);
|
|
2899
|
+
}
|
|
2900
|
+
function backendRuntimeGroupCount(layer) {
|
|
2901
|
+
if (layer.evidenceLevel !== "backend_observed") {
|
|
2902
|
+
return undefined;
|
|
2903
|
+
}
|
|
2904
|
+
const jdbcMatch = /^(\d+)\s+JDBC statement/.exec(layer.label);
|
|
2905
|
+
if (jdbcMatch?.[1]) {
|
|
2906
|
+
return Number(jdbcMatch[1]);
|
|
2907
|
+
}
|
|
2908
|
+
const groupedMatch = /×(\d+)$/.exec(layer.label);
|
|
2909
|
+
if (groupedMatch?.[1]) {
|
|
2910
|
+
return Number(groupedMatch[1]);
|
|
2911
|
+
}
|
|
2912
|
+
return undefined;
|
|
2913
|
+
}
|
|
2914
|
+
function durationValueLabel(_layer, duration) {
|
|
2915
|
+
return `${Math.round(duration)} ms`;
|
|
2916
|
+
}
|
|
2917
|
+
function durationCaption(layer, duration, total, locale, runtimeSource = "browser", runtimeGroupCount) {
|
|
2918
|
+
if (isUntrackedAppLayer(layer))
|
|
2919
|
+
return t(locale, "untrackedRuntime");
|
|
2920
|
+
if (layer.type === "result")
|
|
2921
|
+
return t(locale, "responseMarker");
|
|
2922
|
+
if (layer.type === "action" || layer.type === "api") {
|
|
2923
|
+
return runtimeSource === "server" ? t(locale, "serverRequestSpan") : t(locale, "browserSpan");
|
|
2924
|
+
}
|
|
2925
|
+
if (layer.evidenceLevel === "backend_observed") {
|
|
2926
|
+
if (runtimeGroupCount) {
|
|
2927
|
+
return `${runtimeGroupCount} ${t(locale, "callCount")}`;
|
|
2928
|
+
}
|
|
2929
|
+
return t(locale, "measuredRuntime");
|
|
2930
|
+
}
|
|
2931
|
+
return `${t(locale, "estimate")} · ${Math.round((duration / Math.max(total, 1)) * 100)}%`;
|
|
2932
|
+
}
|
|
2933
|
+
function tabLabel(tab, locale) {
|
|
2934
|
+
if (locale === "ko") {
|
|
2935
|
+
if (tab === "summary")
|
|
2936
|
+
return "요약";
|
|
2937
|
+
if (tab === "timing")
|
|
2938
|
+
return "타이밍";
|
|
2939
|
+
return "다이어그램";
|
|
2940
|
+
}
|
|
2941
|
+
return capitalize(tab);
|
|
2942
|
+
}
|
|
2943
|
+
function executionLabel(execution, locale) {
|
|
2944
|
+
if (execution === "blocked")
|
|
2945
|
+
return t(locale, "blocked");
|
|
2946
|
+
if (execution === "inferred")
|
|
2947
|
+
return t(locale, "inferred");
|
|
2948
|
+
if (execution === "not_proven")
|
|
2949
|
+
return t(locale, "notProven");
|
|
2950
|
+
if (execution === "observed")
|
|
2951
|
+
return t(locale, "observed");
|
|
2952
|
+
if (execution === "scanned")
|
|
2953
|
+
return locale === "ko" ? "소스 매칭" : "source matched";
|
|
2954
|
+
if (execution === "executed")
|
|
2955
|
+
return locale === "ko" ? "실행됨" : "executed";
|
|
2956
|
+
return execution.replace("_", " ");
|
|
2957
|
+
}
|
|
2958
|
+
function confidenceLabel(confidence, locale) {
|
|
2959
|
+
if (locale === "ko") {
|
|
2960
|
+
if (confidence === "high")
|
|
2961
|
+
return "높음";
|
|
2962
|
+
if (confidence === "medium")
|
|
2963
|
+
return "보통";
|
|
2964
|
+
return "낮음";
|
|
2965
|
+
}
|
|
2966
|
+
return capitalize(confidence);
|
|
2967
|
+
}
|
|
2968
|
+
function translateEvidence(value, locale) {
|
|
2969
|
+
if (locale === "en")
|
|
2970
|
+
return value;
|
|
2971
|
+
const normalized = value.toLowerCase();
|
|
2972
|
+
if (normalized.includes("click") || normalized.includes("browser-observed request")) {
|
|
2973
|
+
return "브라우저에서 사용자 액션을 관찰했습니다";
|
|
2974
|
+
}
|
|
2975
|
+
if (normalized.includes("fetch") || normalized.includes("xmlhttprequest")) {
|
|
2976
|
+
return "브라우저 fetch/XHR 요청을 관찰했습니다";
|
|
2977
|
+
}
|
|
2978
|
+
if (normalized.includes("endpoint")) {
|
|
2979
|
+
return "가져온 엔드포인트와 매칭되었습니다";
|
|
2980
|
+
}
|
|
2981
|
+
if (normalized.includes("controller")) {
|
|
2982
|
+
return "컨트롤러 근거가 매칭되었습니다";
|
|
2983
|
+
}
|
|
2984
|
+
if (normalized.includes("backend_observed") || normalized.includes("runtime")) {
|
|
2985
|
+
return "개발용 백엔드 브리지에서 런타임 span을 받았습니다";
|
|
2986
|
+
}
|
|
2987
|
+
if (normalized.includes("source-derived") || normalized.includes("source")) {
|
|
2988
|
+
return "소스 근거 기반 백엔드 매칭입니다";
|
|
2989
|
+
}
|
|
2990
|
+
if (normalized.includes("not a runtime") || normalized.includes("not_proven")) {
|
|
2991
|
+
return "운영 런타임 트레이스가 아니며 실행이 확인되지 않은 구간이 있습니다";
|
|
2992
|
+
}
|
|
2993
|
+
return value;
|
|
2994
|
+
}
|
|
2995
|
+
function compactId(value) {
|
|
2996
|
+
return value.replace(/^browser-request:/, "").slice(0, 8);
|
|
2997
|
+
}
|
|
2998
|
+
function shortPath(value) {
|
|
2999
|
+
return value.length > 30 ? `${value.slice(0, 27)}...` : value;
|
|
3000
|
+
}
|
|
3001
|
+
function formatDateTime(value) {
|
|
3002
|
+
if (!value) {
|
|
3003
|
+
return "Waiting for request";
|
|
3004
|
+
}
|
|
3005
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
3006
|
+
hour: "2-digit",
|
|
3007
|
+
minute: "2-digit",
|
|
3008
|
+
second: "2-digit"
|
|
3009
|
+
}).format(new Date(value));
|
|
3010
|
+
}
|
|
3011
|
+
function formatDuration(value) {
|
|
3012
|
+
return `${Math.round(value ?? 0)} ms`;
|
|
3013
|
+
}
|
|
3014
|
+
function capitalize(value) {
|
|
3015
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
3016
|
+
}
|