@anlyx/ui 0.1.3 → 0.1.5
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 +18 -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 +6354 -0
- package/dist/workspace/ScanTreeMap.d.ts +6 -0
- package/dist/workspace/ScanTreeMap.js +838 -0
- package/dist/workspace/WorkspaceApp.d.ts +8 -0
- package/dist/workspace/WorkspaceApp.js +2293 -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 +6354 -0
- package/package.json +9 -2
|
@@ -0,0 +1,2293 @@
|
|
|
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, Workflow, Zap } from "lucide-react";
|
|
4
|
+
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { ScanTreeMap } from "./ScanTreeMap.js";
|
|
6
|
+
import { buildProjectWorkspaceViewModel } from "./project-view-model.js";
|
|
7
|
+
const WorkspaceLocaleContext = createContext("en");
|
|
8
|
+
const TIMELINE_START_INSET_PERCENT = 2.5;
|
|
9
|
+
const translations = {
|
|
10
|
+
en: {
|
|
11
|
+
appLabel: "Anlyx live workspace",
|
|
12
|
+
sidebarLabel: "Workspace navigation",
|
|
13
|
+
primaryNavLabel: "Primary",
|
|
14
|
+
flows: "Flows",
|
|
15
|
+
docs: "Docs",
|
|
16
|
+
language: "Language",
|
|
17
|
+
flowViews: "Flow views",
|
|
18
|
+
flow: "Flow",
|
|
19
|
+
waiting: "waiting",
|
|
20
|
+
lastScan: "Last import",
|
|
21
|
+
matchedBackendFlow: "Matched backend flow",
|
|
22
|
+
liveWorkspace: "Live workspace",
|
|
23
|
+
captureConnected: "Capture connected",
|
|
24
|
+
waitingForCapture: "Waiting for capture",
|
|
25
|
+
matchedSubtitle: "Browser request first, source-matched backend path follows.",
|
|
26
|
+
readyPrefix: "is ready. Use your local app and Anlyx will stream requests here.",
|
|
27
|
+
readySuffix: "",
|
|
28
|
+
selectedRequest: "Selected request",
|
|
29
|
+
selectedRequestLabel: "Selected request",
|
|
30
|
+
currentPage: "Current page",
|
|
31
|
+
pageUrlNotCaptured: "Page URL not captured",
|
|
32
|
+
requestEvidenceSummary: "Request evidence summary",
|
|
33
|
+
mappedEvidence: "Browser-observed request mapped to imported source evidence.",
|
|
34
|
+
mappedEvidenceWithRuntime: "Browser-observed request mapped to imported source evidence and development runtime spans.",
|
|
35
|
+
controllerPending: "Controller pending",
|
|
36
|
+
flowTiming: "Flow timing",
|
|
37
|
+
totalDuration: "Total duration",
|
|
38
|
+
fit: "Fit",
|
|
39
|
+
focusSlowestSegment: "Focus slowest segment",
|
|
40
|
+
zoomIn: "Zoom in",
|
|
41
|
+
totalDurationOverview: "Total duration overview",
|
|
42
|
+
slowestShownSegment: "Slowest shown segment",
|
|
43
|
+
startsAt: "starts at",
|
|
44
|
+
layer: "Layer",
|
|
45
|
+
node: "Node",
|
|
46
|
+
duration: "Duration",
|
|
47
|
+
notProven: "not proven",
|
|
48
|
+
knownBySourceOnly: "known by source evidence only",
|
|
49
|
+
bottleneck: "Bottleneck",
|
|
50
|
+
slowestSourceSegment: "Slowest source segment",
|
|
51
|
+
blocked: "blocked",
|
|
52
|
+
flowSummary: "Flow summary",
|
|
53
|
+
observedSourceMatchedPath: "Observed / source-matched path",
|
|
54
|
+
summaryMatchedCopy: "Browser-observed request first, then imported source evidence.",
|
|
55
|
+
layers: "layers",
|
|
56
|
+
knownDownstreamPath: "Known downstream path",
|
|
57
|
+
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.",
|
|
58
|
+
flowDiagram: "Flow diagram",
|
|
59
|
+
observedSourceMatchedLegend: "Observed / source-matched path",
|
|
60
|
+
knownDownstreamLegend: "Known downstream (not proven)",
|
|
61
|
+
entryPoint: "Entry point",
|
|
62
|
+
fitView: "Fit view",
|
|
63
|
+
reset: "Reset",
|
|
64
|
+
zoomOut: "Zoom out",
|
|
65
|
+
authDecision: "Auth / Decision",
|
|
66
|
+
backend: "Backend",
|
|
67
|
+
service: "Service",
|
|
68
|
+
repository: "Repository",
|
|
69
|
+
database: "Database",
|
|
70
|
+
untrackedApp: "Untracked app",
|
|
71
|
+
untrackedRuntime: "not yet split into lower layers",
|
|
72
|
+
noScannedNode: "No source node matched",
|
|
73
|
+
downstreamMatched: "Downstream path matched by browser request and source evidence",
|
|
74
|
+
knownScannedNotProven: "Known source path, not proven executed",
|
|
75
|
+
inferred: "Inferred",
|
|
76
|
+
matched: "Matched",
|
|
77
|
+
evidenceInspector: "Evidence inspector",
|
|
78
|
+
anlyxFlow: "Anlyx Flow",
|
|
79
|
+
recentRequests: "Recent requests",
|
|
80
|
+
background: "Background",
|
|
81
|
+
currentRequestWindow: "Current page/action only",
|
|
82
|
+
request: "Request",
|
|
83
|
+
method: "Method",
|
|
84
|
+
path: "Path",
|
|
85
|
+
outcome: "Outcome",
|
|
86
|
+
mappingConfidence: "Mapping confidence",
|
|
87
|
+
evidenceUsed: "Evidence used",
|
|
88
|
+
coverage: "Coverage",
|
|
89
|
+
traceNote: "Trace note",
|
|
90
|
+
keepWorkspaceOpen: "Keep this workspace open, then use your local app. Captured requests will appear here.",
|
|
91
|
+
firstRequestHint: "Click your local app to stream the first browser request.",
|
|
92
|
+
waitingForBrowserRequests: "Waiting for browser requests",
|
|
93
|
+
liveCaptureReady: "Live capture ready",
|
|
94
|
+
noRequestSelected: "No request selected yet",
|
|
95
|
+
noCurrentPageRequests: "No requests observed on this page",
|
|
96
|
+
noCurrentPageRequestsCopy: "Anlyx captured the current page, but no API request has been observed for this page/action yet.",
|
|
97
|
+
emptyWorkspaceCopy: "No flow entries are loaded yet. Add Flow JSON to populate this workspace.",
|
|
98
|
+
requestProcessing: "Request/Processing",
|
|
99
|
+
application: "Application",
|
|
100
|
+
authDecisionLegend: "Auth / Decision",
|
|
101
|
+
result: "Result",
|
|
102
|
+
pending: "Pending",
|
|
103
|
+
ok: "OK",
|
|
104
|
+
unauthorized: "Unauthorized",
|
|
105
|
+
forbidden: "Forbidden",
|
|
106
|
+
conflict: "Conflict",
|
|
107
|
+
serverError: "Server error",
|
|
108
|
+
observed: "observed",
|
|
109
|
+
sourceMatched: "source matched",
|
|
110
|
+
browserSpan: "browser span",
|
|
111
|
+
serverRequestSpan: "server request span",
|
|
112
|
+
browserObserved: "browser observed",
|
|
113
|
+
devRuntimeSpan: "dev runtime span",
|
|
114
|
+
measuredRuntime: "measured runtime",
|
|
115
|
+
callCount: "calls",
|
|
116
|
+
sourceDerivedEstimate: "source-derived estimate",
|
|
117
|
+
responseMarker: "response marker",
|
|
118
|
+
estimate: "estimate",
|
|
119
|
+
browser: "Browser",
|
|
120
|
+
source: "Source",
|
|
121
|
+
backendCoverage: "Backend"
|
|
122
|
+
},
|
|
123
|
+
ko: {
|
|
124
|
+
appLabel: "Anlyx 라이브 워크스페이스",
|
|
125
|
+
sidebarLabel: "워크스페이스 내비게이션",
|
|
126
|
+
primaryNavLabel: "주 메뉴",
|
|
127
|
+
flows: "흐름",
|
|
128
|
+
docs: "문서",
|
|
129
|
+
language: "언어",
|
|
130
|
+
flowViews: "흐름 보기",
|
|
131
|
+
flow: "흐름",
|
|
132
|
+
waiting: "대기 중",
|
|
133
|
+
lastScan: "마지막 가져오기",
|
|
134
|
+
matchedBackendFlow: "매칭된 백엔드 흐름",
|
|
135
|
+
liveWorkspace: "라이브 워크스페이스",
|
|
136
|
+
captureConnected: "캡처 연결됨",
|
|
137
|
+
waitingForCapture: "캡처 대기 중",
|
|
138
|
+
matchedSubtitle: "브라우저 요청을 먼저 보고, 소스 매칭 백엔드 경로를 이어서 보여줍니다.",
|
|
139
|
+
readyPrefix: "준비되었습니다. 로컬 앱을 사용하면 Anlyx가 요청을 이곳으로 스트리밍합니다.",
|
|
140
|
+
readySuffix: "",
|
|
141
|
+
selectedRequest: "선택된 요청",
|
|
142
|
+
selectedRequestLabel: "선택된 요청",
|
|
143
|
+
currentPage: "현재 페이지",
|
|
144
|
+
pageUrlNotCaptured: "페이지 URL 미수집",
|
|
145
|
+
requestEvidenceSummary: "요청 근거 요약",
|
|
146
|
+
mappedEvidence: "브라우저에서 관찰된 요청을 가져온 소스 근거와 매칭했습니다.",
|
|
147
|
+
mappedEvidenceWithRuntime: "브라우저에서 관찰된 요청을 가져온 소스 근거와 개발용 런타임 span에 매칭했습니다.",
|
|
148
|
+
controllerPending: "컨트롤러 확인 대기",
|
|
149
|
+
flowTiming: "흐름 타이밍",
|
|
150
|
+
totalDuration: "전체 소요 시간",
|
|
151
|
+
fit: "맞춤",
|
|
152
|
+
focusSlowestSegment: "가장 느린 구간 강조",
|
|
153
|
+
zoomIn: "확대",
|
|
154
|
+
totalDurationOverview: "전체 소요 시간 요약",
|
|
155
|
+
slowestShownSegment: "가장 긴 구간",
|
|
156
|
+
startsAt: "시작",
|
|
157
|
+
layer: "레이어",
|
|
158
|
+
node: "노드",
|
|
159
|
+
duration: "소요 시간",
|
|
160
|
+
notProven: "실행 확인 안 됨",
|
|
161
|
+
knownBySourceOnly: "소스 근거로만 확인됨",
|
|
162
|
+
bottleneck: "병목",
|
|
163
|
+
slowestSourceSegment: "가장 긴 소스 구간",
|
|
164
|
+
blocked: "차단됨",
|
|
165
|
+
flowSummary: "흐름 요약",
|
|
166
|
+
observedSourceMatchedPath: "관찰 / 소스 매칭 경로",
|
|
167
|
+
summaryMatchedCopy: "브라우저 요청을 먼저 보여주고, 이어서 가져온 소스 근거를 보여줍니다.",
|
|
168
|
+
layers: "개 레이어",
|
|
169
|
+
knownDownstreamPath: "알려진 하위 경로",
|
|
170
|
+
summaryNote: "Anlyx는 브라우저에서 관찰된 요청을 가져온 소스 근거와 연결합니다. 소스 매칭 행은 운영 런타임 트레이스가 아니며, 흐리게 표시된 행은 실제 실행이 확인되지 않았습니다.",
|
|
171
|
+
flowDiagram: "흐름 다이어그램",
|
|
172
|
+
observedSourceMatchedLegend: "관찰 / 소스 매칭 경로",
|
|
173
|
+
knownDownstreamLegend: "알려진 하위 경로(실행 미확인)",
|
|
174
|
+
entryPoint: "진입점",
|
|
175
|
+
fitView: "화면 맞춤",
|
|
176
|
+
reset: "초기화",
|
|
177
|
+
zoomOut: "축소",
|
|
178
|
+
authDecision: "인증 / 결정",
|
|
179
|
+
backend: "백엔드",
|
|
180
|
+
service: "서비스",
|
|
181
|
+
repository: "레포지토리",
|
|
182
|
+
database: "데이터베이스",
|
|
183
|
+
untrackedApp: "미분해 앱 시간",
|
|
184
|
+
untrackedRuntime: "하위 레이어로 아직 분해되지 않음",
|
|
185
|
+
noScannedNode: "매칭된 소스 노드 없음",
|
|
186
|
+
downstreamMatched: "브라우저 요청과 소스 근거로 하위 경로가 매칭되었습니다",
|
|
187
|
+
knownScannedNotProven: "소스 근거로 알려진 경로지만 실행은 확인되지 않았습니다",
|
|
188
|
+
inferred: "추론됨",
|
|
189
|
+
matched: "매칭됨",
|
|
190
|
+
evidenceInspector: "근거 인스펙터",
|
|
191
|
+
anlyxFlow: "Anlyx 흐름",
|
|
192
|
+
recentRequests: "최근 요청",
|
|
193
|
+
background: "백그라운드",
|
|
194
|
+
currentRequestWindow: "현재 페이지/액션만",
|
|
195
|
+
request: "요청",
|
|
196
|
+
method: "메서드",
|
|
197
|
+
path: "경로",
|
|
198
|
+
outcome: "결과",
|
|
199
|
+
mappingConfidence: "매핑 신뢰도",
|
|
200
|
+
evidenceUsed: "사용된 근거",
|
|
201
|
+
coverage: "근거 범위",
|
|
202
|
+
traceNote: "트레이스 참고",
|
|
203
|
+
keepWorkspaceOpen: "이 워크스페이스를 열어둔 뒤 로컬 앱을 사용하세요. 캡처된 요청이 이곳에 표시됩니다.",
|
|
204
|
+
firstRequestHint: "로컬 앱을 클릭하면 첫 브라우저 요청이 스트리밍됩니다.",
|
|
205
|
+
waitingForBrowserRequests: "브라우저 요청 대기 중",
|
|
206
|
+
liveCaptureReady: "라이브 캡처 준비됨",
|
|
207
|
+
noRequestSelected: "아직 선택된 요청이 없습니다",
|
|
208
|
+
noCurrentPageRequests: "이 페이지에서 관찰된 요청이 없습니다",
|
|
209
|
+
noCurrentPageRequestsCopy: "현재 페이지는 감지됐지만, 이 페이지/액션에 연결된 API 요청은 아직 관찰되지 않았습니다.",
|
|
210
|
+
emptyWorkspaceCopy: "아직 불러온 flow entry가 없습니다. Flow JSON을 추가하면 이 워크스페이스가 채워집니다.",
|
|
211
|
+
requestProcessing: "요청/처리",
|
|
212
|
+
application: "애플리케이션",
|
|
213
|
+
authDecisionLegend: "인증 / 결정",
|
|
214
|
+
result: "결과",
|
|
215
|
+
pending: "대기 중",
|
|
216
|
+
ok: "정상",
|
|
217
|
+
unauthorized: "인증 필요",
|
|
218
|
+
forbidden: "권한 없음",
|
|
219
|
+
conflict: "충돌",
|
|
220
|
+
serverError: "서버 오류",
|
|
221
|
+
observed: "관찰됨",
|
|
222
|
+
sourceMatched: "소스 매칭",
|
|
223
|
+
browserSpan: "브라우저 구간",
|
|
224
|
+
serverRequestSpan: "서버 요청 구간",
|
|
225
|
+
browserObserved: "브라우저 관찰",
|
|
226
|
+
devRuntimeSpan: "개발 런타임 구간",
|
|
227
|
+
measuredRuntime: "실측 시간",
|
|
228
|
+
callCount: "회 호출",
|
|
229
|
+
sourceDerivedEstimate: "소스 기반 추정",
|
|
230
|
+
responseMarker: "응답 지점",
|
|
231
|
+
estimate: "추정",
|
|
232
|
+
browser: "브라우저",
|
|
233
|
+
source: "소스",
|
|
234
|
+
backendCoverage: "백엔드"
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
function useWorkspaceLocale() {
|
|
238
|
+
return useContext(WorkspaceLocaleContext);
|
|
239
|
+
}
|
|
240
|
+
function t(locale, key) {
|
|
241
|
+
return translations[locale][key] ?? translations.en[key];
|
|
242
|
+
}
|
|
243
|
+
const projectChromeLocales = [
|
|
244
|
+
{ value: "en", label: "English", shortLabel: "EN" },
|
|
245
|
+
{ value: "ko", label: "한국어", shortLabel: "KO" },
|
|
246
|
+
{ value: "zh", label: "中文", shortLabel: "ZH" },
|
|
247
|
+
{ value: "ja", label: "日本語", shortLabel: "JA" },
|
|
248
|
+
{ value: "fr", label: "Français", shortLabel: "FR" }
|
|
249
|
+
];
|
|
250
|
+
const projectChromeTranslations = {
|
|
251
|
+
en: {
|
|
252
|
+
agent: "AI Agent",
|
|
253
|
+
available: "Available",
|
|
254
|
+
disabled: "Disabled",
|
|
255
|
+
language: "Language",
|
|
256
|
+
lastAnalysis: "Last analysis",
|
|
257
|
+
map: "Map",
|
|
258
|
+
pages: "Pages",
|
|
259
|
+
source: "Source",
|
|
260
|
+
timing: "Timing",
|
|
261
|
+
upToDate: "Up to date",
|
|
262
|
+
pagesAnalyzed: "pages analyzed"
|
|
263
|
+
},
|
|
264
|
+
ko: {
|
|
265
|
+
agent: "AI Agent",
|
|
266
|
+
available: "사용 가능",
|
|
267
|
+
disabled: "비활성",
|
|
268
|
+
language: "언어",
|
|
269
|
+
lastAnalysis: "마지막 분석",
|
|
270
|
+
map: "맵",
|
|
271
|
+
pages: "페이지",
|
|
272
|
+
source: "소스",
|
|
273
|
+
timing: "타이밍",
|
|
274
|
+
upToDate: "최신 상태",
|
|
275
|
+
pagesAnalyzed: "개 페이지 분석됨"
|
|
276
|
+
},
|
|
277
|
+
zh: {
|
|
278
|
+
agent: "AI Agent",
|
|
279
|
+
available: "可用",
|
|
280
|
+
disabled: "已停用",
|
|
281
|
+
language: "语言",
|
|
282
|
+
lastAnalysis: "最后分析",
|
|
283
|
+
map: "地图",
|
|
284
|
+
pages: "页面",
|
|
285
|
+
source: "来源",
|
|
286
|
+
timing: "计时",
|
|
287
|
+
upToDate: "已是最新",
|
|
288
|
+
pagesAnalyzed: "个页面已分析"
|
|
289
|
+
},
|
|
290
|
+
ja: {
|
|
291
|
+
agent: "AI Agent",
|
|
292
|
+
available: "利用可能",
|
|
293
|
+
disabled: "無効",
|
|
294
|
+
language: "言語",
|
|
295
|
+
lastAnalysis: "最終分析",
|
|
296
|
+
map: "マップ",
|
|
297
|
+
pages: "ページ",
|
|
298
|
+
source: "ソース",
|
|
299
|
+
timing: "タイミング",
|
|
300
|
+
upToDate: "最新",
|
|
301
|
+
pagesAnalyzed: "ページ分析済み"
|
|
302
|
+
},
|
|
303
|
+
fr: {
|
|
304
|
+
agent: "Agent IA",
|
|
305
|
+
available: "Disponible",
|
|
306
|
+
disabled: "Désactivé",
|
|
307
|
+
language: "Langue",
|
|
308
|
+
lastAnalysis: "Dernière analyse",
|
|
309
|
+
map: "Carte",
|
|
310
|
+
pages: "Pages",
|
|
311
|
+
source: "Source",
|
|
312
|
+
timing: "Timing",
|
|
313
|
+
upToDate: "À jour",
|
|
314
|
+
pagesAnalyzed: "pages analysées"
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
function tp(locale, key) {
|
|
318
|
+
return projectChromeTranslations[locale]?.[key] ?? projectChromeTranslations.en[key];
|
|
319
|
+
}
|
|
320
|
+
function projectDefaultLocale(data) {
|
|
321
|
+
return data.dictionary?.defaultLanguage ?? "en";
|
|
322
|
+
}
|
|
323
|
+
const layerOrder = [
|
|
324
|
+
"action",
|
|
325
|
+
"api",
|
|
326
|
+
"controller",
|
|
327
|
+
"auth",
|
|
328
|
+
"decision",
|
|
329
|
+
"service",
|
|
330
|
+
"repository",
|
|
331
|
+
"database",
|
|
332
|
+
"result"
|
|
333
|
+
];
|
|
334
|
+
const layerIcons = {
|
|
335
|
+
action: Zap,
|
|
336
|
+
api: Network,
|
|
337
|
+
auth: Lock,
|
|
338
|
+
controller: FileCode2,
|
|
339
|
+
database: Database,
|
|
340
|
+
decision: Lock,
|
|
341
|
+
repository: Box,
|
|
342
|
+
result: Flag,
|
|
343
|
+
service: Layers3
|
|
344
|
+
};
|
|
345
|
+
export function WorkspaceApp({ data, projectData, streamUrl = "/_anlyx/events/stream", initialRecords = [] }) {
|
|
346
|
+
if (projectData) {
|
|
347
|
+
return _jsx(ProjectWorkspacePreview, { data: projectData });
|
|
348
|
+
}
|
|
349
|
+
if (!data) {
|
|
350
|
+
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." })] }) }));
|
|
351
|
+
}
|
|
352
|
+
return _jsx(LegacyWorkspaceApp, { data: data, initialRecords: initialRecords, streamUrl: streamUrl });
|
|
353
|
+
}
|
|
354
|
+
function LegacyWorkspaceApp({ data, streamUrl, initialRecords }) {
|
|
355
|
+
const [records, setRecords] = useState(initialRecords);
|
|
356
|
+
const [selectedId, setSelectedId] = useState(initialRecords[0]?.id);
|
|
357
|
+
const [tab, setTab] = useState("timing");
|
|
358
|
+
const [activeView, setActiveView] = useState("flows");
|
|
359
|
+
const [locale, setLocale] = useState("en");
|
|
360
|
+
const [pageContext, setPageContext] = useState();
|
|
361
|
+
const currentPageRecords = useMemo(() => recordsForPageContext(records, pageContext), [records, pageContext]);
|
|
362
|
+
const currentPagePrimaryRecords = useMemo(() => currentPageRecords.filter(isPrimaryRecord), [currentPageRecords]);
|
|
363
|
+
const selectedRecord = currentPagePrimaryRecords.find((record) => record.id === selectedId) ??
|
|
364
|
+
currentPagePrimaryRecords[0];
|
|
365
|
+
const scopedRecords = scopedRecentRecords(currentPageRecords, selectedRecord);
|
|
366
|
+
useEffect(() => {
|
|
367
|
+
if (currentPagePrimaryRecords.length === 0) {
|
|
368
|
+
if (pageContext && selectedId) {
|
|
369
|
+
setSelectedId(undefined);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (!selectedId || !currentPagePrimaryRecords.some((record) => record.id === selectedId)) {
|
|
374
|
+
const firstRecord = currentPagePrimaryRecords[0];
|
|
375
|
+
if (firstRecord) {
|
|
376
|
+
setSelectedId(firstRecord.id);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}, [currentPagePrimaryRecords, pageContext, selectedId]);
|
|
380
|
+
useEffect(() => {
|
|
381
|
+
if (typeof EventSource === "undefined") {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
const source = new EventSource(streamUrl);
|
|
385
|
+
const handlePageContext = (event) => {
|
|
386
|
+
const context = parsePageContextEvent(event.data);
|
|
387
|
+
if (context) {
|
|
388
|
+
setPageContext(context);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const handleFlow = (event) => {
|
|
392
|
+
const record = parseFlowRecord(event.data);
|
|
393
|
+
if (!record) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
setRecords((current) => [record, ...current.filter((item) => item.id !== record.id)]);
|
|
397
|
+
setSelectedId((current) => {
|
|
398
|
+
if (isPrimaryRecord(record)) {
|
|
399
|
+
return record.id;
|
|
400
|
+
}
|
|
401
|
+
return current;
|
|
402
|
+
});
|
|
403
|
+
};
|
|
404
|
+
source.addEventListener("page-context", handlePageContext);
|
|
405
|
+
source.addEventListener("flow", handleFlow);
|
|
406
|
+
return () => {
|
|
407
|
+
source.removeEventListener("page-context", handlePageContext);
|
|
408
|
+
source.removeEventListener("flow", handleFlow);
|
|
409
|
+
source.close();
|
|
410
|
+
};
|
|
411
|
+
}, [streamUrl]);
|
|
412
|
+
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 })] })) })] }) }));
|
|
413
|
+
}
|
|
414
|
+
function ProjectWorkspacePreview({ data }) {
|
|
415
|
+
const [activeTab, setActiveTab] = useState("pages");
|
|
416
|
+
const [projectLocale, setProjectLocale] = useState(() => projectDefaultLocale(data));
|
|
417
|
+
const [selectedPageId, setSelectedPageId] = useState(data.pages[0]?.id);
|
|
418
|
+
const model = useMemo(() => buildProjectWorkspaceViewModel(data, selectedPageId), [data, selectedPageId]);
|
|
419
|
+
const rawJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
setProjectLocale(projectDefaultLocale(data));
|
|
422
|
+
}, [data]);
|
|
423
|
+
return (_jsxs("main", { className: "project-workspace-preview", role: "application", "aria-label": "Anlyx project workspace", children: [_jsx(ProjectTopBar, { model: model, locale: projectLocale, onLocaleChange: setProjectLocale }), _jsx(ProjectTabs, { activeTab: activeTab, locale: projectLocale, onTabChange: setActiveTab }), activeTab === "pages" ? (_jsx(ProjectPagesWorkspace, { model: model, selectedPageId: model.selectedPage?.page.id, onPageSelect: setSelectedPageId, onOpenMap: () => setActiveTab("map") })) : activeTab === "map" ? (_jsx(ProjectMapView, { data: data, model: model })) : (_jsx(ProjectJsonView, { data: data, rawJson: rawJson, model: model })), _jsx(ProjectStatusBar, { locale: projectLocale, model: model })] }));
|
|
424
|
+
}
|
|
425
|
+
function readProjectMetadataString(model, key) {
|
|
426
|
+
const value = model.project.metadata?.[key];
|
|
427
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
428
|
+
}
|
|
429
|
+
function projectAgentName(model) {
|
|
430
|
+
return model.project.generatedBy?.name ?? model.project.generatedBy?.kind ?? "AI Agent";
|
|
431
|
+
}
|
|
432
|
+
function projectSourceFile(model) {
|
|
433
|
+
return readProjectMetadataString(model, "sourceFile") ?? "anlyx.project.json";
|
|
434
|
+
}
|
|
435
|
+
function ProjectTopBar({ locale, model, onLocaleChange }) {
|
|
436
|
+
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] }), _jsx("strong", { children: "OSS" })] }), _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" }), _jsx("div", { className: "project-topbar__actions", children: _jsxs("label", { className: "project-language-select", children: [_jsx(Languages, { size: 15 }), _jsx("span", { children: tp(locale, "language") }), _jsx("select", { "aria-label": tp(locale, "language"), value: locale, onChange: (event) => onLocaleChange(event.currentTarget.value), children: projectChromeLocales.map((item) => (_jsx("option", { value: item.value, children: item.shortLabel }, item.value))) })] }) })] }));
|
|
437
|
+
}
|
|
438
|
+
function ProjectTabs({ activeTab, locale, onTabChange }) {
|
|
439
|
+
return (_jsx("div", { className: "project-tabs", role: "tablist", "aria-label": "Project workspace views", children: ["pages", "map", "json"].map((tab) => (_jsx("button", { "aria-selected": activeTab === tab, className: activeTab === tab ? "is-active" : "", role: "tab", type: "button", onClick: () => onTabChange(tab), children: tab === "pages" ? tp(locale, "pages") : tab === "map" ? tp(locale, "map") : "JSON" }, tab))) }));
|
|
440
|
+
}
|
|
441
|
+
function ProjectPagesWorkspace({ model, onOpenMap, onPageSelect, selectedPageId }) {
|
|
442
|
+
return (_jsxs("div", { className: "project-pages-layout", children: [_jsx(ProjectPageIndex, { model: model, selectedPageId: selectedPageId, onPageSelect: onPageSelect }), _jsx(ProjectPagesView, { selectedPage: model.selectedPage, onOpenMap: onOpenMap })] }));
|
|
443
|
+
}
|
|
444
|
+
function ProjectPageIndex({ model, onPageSelect, selectedPageId }) {
|
|
445
|
+
return (_jsxs("aside", { className: "project-page-index-panel", "aria-label": "Page index", children: [_jsxs("div", { className: "project-panel-title", children: [_jsx("h2", { children: "Page Index" }), _jsx("button", { "aria-label": "Filter pages", type: "button", children: _jsx(PanelLeft, { size: 15 }) })] }), _jsxs("div", { className: "project-page-index__search", children: [_jsx(Search, { size: 16 }), _jsx("span", { children: "Search pages..." }), _jsx("kbd", { children: "/" })] }), _jsx("nav", { className: "project-page-index", "aria-label": "Project pages", children: model.pageGroups.map((group) => (_jsxs("section", { children: [_jsxs("h2", { 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 })] })] }));
|
|
446
|
+
}
|
|
447
|
+
function ProjectPagesView({ onOpenMap, selectedPage }) {
|
|
448
|
+
const firstRequestId = selectedPage?.requests[0]?.id;
|
|
449
|
+
const [selectedRequestId, setSelectedRequestId] = useState(firstRequestId);
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
setSelectedRequestId(firstRequestId);
|
|
452
|
+
}, [firstRequestId, selectedPage?.page.id]);
|
|
453
|
+
if (!selectedPage) {
|
|
454
|
+
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." })] }));
|
|
455
|
+
}
|
|
456
|
+
const selectedRequest = selectedPage.requests.find((request) => request.id === selectedRequestId) ??
|
|
457
|
+
selectedPage.requests[0];
|
|
458
|
+
const selectedRequestLabel = selectedRequest?.path ?? selectedRequest?.label ?? selectedRequest?.id ?? "selected request";
|
|
459
|
+
const selectedRequestFlows = selectedRequest
|
|
460
|
+
? selectedPage.flows.filter((flow) => flow.request?.id === selectedRequest.id ||
|
|
461
|
+
(flow.request?.path && flow.request.path === selectedRequest.path))
|
|
462
|
+
: [];
|
|
463
|
+
const visibleFlows = selectedRequest ? selectedRequestFlows : selectedPage.flows;
|
|
464
|
+
return (_jsxs("div", { className: "project-page-workspace", children: [_jsxs("header", { className: "project-page-workspace__title", children: [_jsxs("div", { children: [_jsxs("span", { children: [selectedPage.areaName, " / Page detail"] }), _jsx("h1", { children: selectedPage.page.title }), _jsx("code", { children: selectedPage.page.path })] }), _jsxs("div", { children: [_jsx("span", { className: "project-badge project-badge--green", children: "AI-authored" }), _jsxs("button", { type: "button", onClick: onOpenMap, children: [_jsx(Network, { size: 16 }), "View in Map"] })] })] }), _jsx(ProjectSection, { title: "Story", children: _jsx("p", { className: "project-story-copy", children: selectedPage.page.description ??
|
|
465
|
+
"No story was authored for this page yet. Ask the project Agent to describe what this page does and why it exists." }) }), _jsx(ProjectSection, { title: "Features", children: _jsx("div", { className: "project-feature-grid", children: selectedPage.features.length > 0 ? (selectedPage.features.map((feature) => (_jsx(ProjectFeatureCard, { feature: feature }, feature.id)))) : (_jsx("p", { className: "project-muted", children: "No features authored for this page." })) }) }), _jsxs(ProjectSection, { title: "Requests", children: [_jsxs("div", { className: "project-section__summary", children: [_jsxs("span", { children: [selectedPage.requests.length, " requests"] }), _jsx("span", { children: "Select a request to show its backend path." })] }), _jsx("div", { className: "project-request-grid", children: selectedPage.requests.length > 0 ? (selectedPage.requests.map((request) => (_jsx(ProjectRequestCard, { isSelected: selectedRequest?.id === request.id, request: request, onSelect: () => setSelectedRequestId(request.id) }, request.id)))) : (_jsx("p", { className: "project-muted", children: "No requests linked to this page." })) })] }), _jsx(ProjectSection, { meta: selectedRequest
|
|
466
|
+
? `${selectedRequest.method ?? "HTTP"} ${selectedRequestLabel}`
|
|
467
|
+
: undefined, title: "Flow summary", children: visibleFlows.length > 0 ? (visibleFlows.map((flow) => _jsx(ProjectFlowTrace, { flow: flow }, flow.id))) : (_jsx("p", { className: "project-muted", children: "No backend flow linked to this request." })) }), _jsx(ProjectSection, { title: "Evidence summary", children: _jsxs("div", { className: "project-evidence-row", children: [_jsx(EvidenceChip, { label: "Observed", value: selectedPage.evidenceSummary.observed }), _jsx(EvidenceChip, { label: "Found in code", value: selectedPage.evidenceSummary.sourceMatched }), _jsx(EvidenceChip, { label: "Agent inferred", value: selectedPage.evidenceSummary.agentInferred }), _jsx(EvidenceChip, { label: "Not proven", value: selectedPage.evidenceSummary.notProven }), _jsx(EvidenceChip, { label: "Unknown", value: selectedPage.evidenceSummary.unknown })] }) })] }));
|
|
468
|
+
}
|
|
469
|
+
function ProjectSection({ children, meta, title }) {
|
|
470
|
+
return (_jsxs("section", { className: "project-section", children: [_jsxs("header", { className: "project-section__header", children: [_jsx("h3", { children: title }), meta ? _jsx("span", { children: meta }) : null] }), children] }));
|
|
471
|
+
}
|
|
472
|
+
function ProjectFeatureCard({ feature }) {
|
|
473
|
+
return (_jsxs("article", { className: "project-feature-card", children: [_jsx(FileText, { size: 20 }), _jsxs("div", { children: [_jsx("strong", { children: feature.name }), _jsx("p", { children: feature.description ?? "No feature description authored yet." })] }), _jsxs("span", { children: [feature.requests.length, " requests"] })] }));
|
|
474
|
+
}
|
|
475
|
+
function ProjectRequestCard({ isSelected, onSelect, request }) {
|
|
476
|
+
const label = request.path ?? request.label ?? request.id;
|
|
477
|
+
return (_jsxs("button", { className: `project-request-card project-request-card--${request.role} ${isSelected ? "is-selected" : ""}`, type: "button", onClick: onSelect, children: [_jsx("header", { children: _jsx("span", { children: request.role }) }), _jsxs("strong", { children: [_jsx("em", { children: request.method ?? "HTTP" }), label] }), _jsxs("footer", { children: [_jsx("small", { children: "Purpose" }), _jsx("b", { children: request.purpose })] })] }));
|
|
478
|
+
}
|
|
479
|
+
function ProjectFlowTrace({ flow }) {
|
|
480
|
+
const layers = flow.layers.length > 0 ? flow.layers : [];
|
|
481
|
+
return (_jsxs("article", { className: "project-flow-trace", children: [_jsxs("header", { children: [_jsx("span", { children: flow.request?.role ?? "flow" }), _jsx("strong", { children: flow.name ?? flow.request?.path ?? flow.id })] }), _jsx("div", { className: "project-flow-trace__layers", children: layers.map((layer, index) => (_jsx(ProjectFlowLayerCard, { index: index, layer: layer }, layer.id))) })] }));
|
|
482
|
+
}
|
|
483
|
+
function ProjectFlowLayerCard({ index, layer }) {
|
|
484
|
+
return (_jsxs("div", { className: `project-flow-layer project-flow-layer--${layer.kind}`, children: [_jsx("span", { children: index + 1 }), _jsx("strong", { children: layer.kind }), _jsx("em", { children: layer.label }), _jsx("small", { children: evidenceStatusLabel(layer.status) })] }));
|
|
485
|
+
}
|
|
486
|
+
function EvidenceChip({ label, value }) {
|
|
487
|
+
return (_jsxs("span", { className: "project-evidence-chip", children: [_jsx(Check, { size: 15 }), label, _jsx("strong", { children: value })] }));
|
|
488
|
+
}
|
|
489
|
+
function ProjectMapView({ data, model }) {
|
|
490
|
+
const graph = useMemo(() => buildProjectArchitectureReactFlow(data), [data]);
|
|
491
|
+
if (data.architecture.nodes.length === 0) {
|
|
492
|
+
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" })] })] })] }));
|
|
493
|
+
}
|
|
494
|
+
return (_jsxs("section", { className: "project-architecture-map project-screen-panel", "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 only nodes and edges from project JSON." })] }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "Nodes" }), _jsx("dd", { children: data.architecture.nodes.length })] }), _jsxs("div", { children: [_jsx("dt", { children: "Edges" }), _jsx("dd", { children: graph.edges.length })] }), _jsxs("div", { children: [_jsx("dt", { children: "Pages" }), _jsx("dd", { children: model.totals.pages })] })] })] }), _jsx("div", { className: "project-architecture-map__canvas", "data-testid": "project-architecture-map", children: _jsx(ReactFlow, { className: "project-architecture-react-flow", edgeTypes: projectArchitectureEdgeTypes, edges: graph.edges, fitView: true, fitViewOptions: { padding: 0.18 }, maxZoom: 1.45, minZoom: 0.45, nodes: graph.nodes, nodesConnectable: false, nodesDraggable: false, nodeTypes: projectArchitectureNodeTypes, proOptions: { hideAttribution: true } }) })] }));
|
|
495
|
+
}
|
|
496
|
+
const projectArchitectureNodeTypes = { projectArchitectureNode: ProjectArchitectureNode };
|
|
497
|
+
const projectArchitectureEdgeTypes = { projectArchitectureEdge: ProjectArchitectureEdge };
|
|
498
|
+
const projectArchitectureLayerX = {
|
|
499
|
+
frontend: 32,
|
|
500
|
+
request: 220,
|
|
501
|
+
api: 410,
|
|
502
|
+
controller: 600,
|
|
503
|
+
handler: 600,
|
|
504
|
+
middleware: 600,
|
|
505
|
+
service: 810,
|
|
506
|
+
policy: 810,
|
|
507
|
+
mapper: 810,
|
|
508
|
+
cache: 810,
|
|
509
|
+
queue: 810,
|
|
510
|
+
job: 810,
|
|
511
|
+
external: 810,
|
|
512
|
+
repository: 1030,
|
|
513
|
+
database: 1240,
|
|
514
|
+
result: 1450,
|
|
515
|
+
unknown: 810
|
|
516
|
+
};
|
|
517
|
+
function buildProjectArchitectureReactFlow(data) {
|
|
518
|
+
const nodeIds = new Set(data.architecture.nodes.map((node) => node.id));
|
|
519
|
+
const validEdges = data.architecture.edges.filter((edge) => nodeIds.has(edge.source) && nodeIds.has(edge.target));
|
|
520
|
+
const connectionCounts = new Map();
|
|
521
|
+
validEdges.forEach((edge) => {
|
|
522
|
+
connectionCounts.set(edge.source, (connectionCounts.get(edge.source) ?? 0) + 1);
|
|
523
|
+
connectionCounts.set(edge.target, (connectionCounts.get(edge.target) ?? 0) + 1);
|
|
524
|
+
});
|
|
525
|
+
const domains = Array.from(new Set(data.architecture.nodes.map((node) => node.domain ?? "Project")));
|
|
526
|
+
const domainIndex = new Map(domains.map((domain, index) => [domain, index]));
|
|
527
|
+
const slotCounts = new Map();
|
|
528
|
+
const nodes = data.architecture.nodes.map((node) => {
|
|
529
|
+
const domain = node.domain ?? "Project";
|
|
530
|
+
const layer = node.kind in projectArchitectureLayerX ? node.kind : "unknown";
|
|
531
|
+
const layerX = projectArchitectureLayerX[layer] ?? 810;
|
|
532
|
+
const slotKey = `${domain}:${layer}`;
|
|
533
|
+
const slot = slotCounts.get(slotKey) ?? 0;
|
|
534
|
+
slotCounts.set(slotKey, slot + 1);
|
|
535
|
+
return {
|
|
536
|
+
id: node.id,
|
|
537
|
+
type: "projectArchitectureNode",
|
|
538
|
+
position: {
|
|
539
|
+
x: layerX,
|
|
540
|
+
y: 54 + (domainIndex.get(domain) ?? 0) * 138 + slot * 46
|
|
541
|
+
},
|
|
542
|
+
data: {
|
|
543
|
+
node: {
|
|
544
|
+
...node,
|
|
545
|
+
connectionCount: connectionCounts.get(node.id) ?? 0
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
draggable: false,
|
|
549
|
+
selectable: false
|
|
550
|
+
};
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
nodes,
|
|
554
|
+
edges: validEdges.map((edge) => ({
|
|
555
|
+
id: edge.id,
|
|
556
|
+
source: edge.source,
|
|
557
|
+
sourceHandle: "right",
|
|
558
|
+
target: edge.target,
|
|
559
|
+
targetHandle: "left",
|
|
560
|
+
type: "projectArchitectureEdge",
|
|
561
|
+
data: { edge },
|
|
562
|
+
focusable: false,
|
|
563
|
+
selectable: false
|
|
564
|
+
}))
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function ProjectArchitectureNode({ data }) {
|
|
568
|
+
const node = data.node;
|
|
569
|
+
const label = node.displayLabel ?? node.label;
|
|
570
|
+
const Icon = projectArchitectureNodeIcon(node.kind);
|
|
571
|
+
return (_jsxs("article", { className: `project-architecture-node project-architecture-node--${node.kind}`, title: `${node.label}${node.source?.filePath ? ` · ${node.source.filePath}` : ""}`, children: [_jsx(Handle, { className: "project-architecture-handle", id: "left", position: Position.Left, type: "target" }), _jsx("span", { className: "project-architecture-node__icon", children: _jsx(Icon, { size: 15 }) }), _jsxs("div", { children: [_jsx("strong", { children: label }), _jsx("span", { children: node.kind })] }), node.connectionCount > 2 ? _jsx("em", { children: node.connectionCount }) : null, _jsx(Handle, { className: "project-architecture-handle", id: "right", position: Position.Right, type: "source" })] }));
|
|
572
|
+
}
|
|
573
|
+
function ProjectArchitectureEdge({ data, sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY }) {
|
|
574
|
+
const [path] = getSmoothStepPath({
|
|
575
|
+
borderRadius: 12,
|
|
576
|
+
offset: 18,
|
|
577
|
+
sourcePosition,
|
|
578
|
+
sourceX,
|
|
579
|
+
sourceY,
|
|
580
|
+
targetPosition,
|
|
581
|
+
targetX,
|
|
582
|
+
targetY
|
|
583
|
+
});
|
|
584
|
+
const role = data?.edge.role ?? "primary";
|
|
585
|
+
return (_jsx(BaseEdge, { className: `project-architecture-edge project-architecture-edge--${role}`, path: path }));
|
|
586
|
+
}
|
|
587
|
+
function projectArchitectureNodeIcon(kind) {
|
|
588
|
+
if (kind === "database") {
|
|
589
|
+
return Database;
|
|
590
|
+
}
|
|
591
|
+
if (kind === "repository") {
|
|
592
|
+
return Box;
|
|
593
|
+
}
|
|
594
|
+
if (kind === "service" || kind === "policy" || kind === "mapper") {
|
|
595
|
+
return Layers3;
|
|
596
|
+
}
|
|
597
|
+
if (kind === "controller" || kind === "handler" || kind === "middleware") {
|
|
598
|
+
return FileCode2;
|
|
599
|
+
}
|
|
600
|
+
if (kind === "api" || kind === "request") {
|
|
601
|
+
return Network;
|
|
602
|
+
}
|
|
603
|
+
if (kind === "result") {
|
|
604
|
+
return Flag;
|
|
605
|
+
}
|
|
606
|
+
return Circle;
|
|
607
|
+
}
|
|
608
|
+
function ProjectJsonView({ data, model, rawJson }) {
|
|
609
|
+
const jsonFiles = useMemo(() => projectJsonFiles(data, model, rawJson), [data, model, rawJson]);
|
|
610
|
+
const [selectedJsonFileId, setSelectedJsonFileId] = useState(jsonFiles[0]?.id ?? "project");
|
|
611
|
+
const selectedJsonFile = jsonFiles.find((file) => file.id === selectedJsonFileId) ?? jsonFiles[0];
|
|
612
|
+
const activeJson = selectedJsonFile?.content ?? rawJson;
|
|
613
|
+
const lines = activeJson.split("\n");
|
|
614
|
+
const inventory = useMemo(() => projectJsonInventoryItems(data, model), [data, model]);
|
|
615
|
+
const EditorFileIcon = selectedJsonFile?.icon ?? Code2;
|
|
616
|
+
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) => {
|
|
617
|
+
const Icon = file.icon;
|
|
618
|
+
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));
|
|
619
|
+
}) })] }), _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: "Valid", tone: "green" })] }), _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))) }) })] })] }));
|
|
620
|
+
}
|
|
621
|
+
function JsonInventoryItem({ item }) {
|
|
622
|
+
const Icon = item.icon;
|
|
623
|
+
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 })] }));
|
|
624
|
+
}
|
|
625
|
+
function projectJsonFiles(data, model, rawJson) {
|
|
626
|
+
const files = [
|
|
627
|
+
{
|
|
628
|
+
id: "project",
|
|
629
|
+
name: projectSourceFile(model),
|
|
630
|
+
description: "Complete project document",
|
|
631
|
+
countLabel: "full",
|
|
632
|
+
content: rawJson,
|
|
633
|
+
icon: Code2
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
id: "index",
|
|
637
|
+
name: ".anlyx/project/index.json",
|
|
638
|
+
description: "Schema and project metadata",
|
|
639
|
+
countLabel: data.schemaVersion,
|
|
640
|
+
content: projectJsonString({ schemaVersion: data.schemaVersion, project: data.project }),
|
|
641
|
+
icon: Folder
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
id: "areas",
|
|
645
|
+
name: ".anlyx/project/areas.json",
|
|
646
|
+
description: "Project areas",
|
|
647
|
+
countLabel: String(model.totals.areas),
|
|
648
|
+
content: projectJsonString(data.areas),
|
|
649
|
+
icon: BriefcaseBusiness
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
id: "pages",
|
|
653
|
+
name: ".anlyx/project/pages.json",
|
|
654
|
+
description: "Page index and routes",
|
|
655
|
+
countLabel: String(model.totals.pages),
|
|
656
|
+
content: projectJsonString(data.pages),
|
|
657
|
+
icon: BookOpen
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
id: "features",
|
|
661
|
+
name: ".anlyx/project/features.json",
|
|
662
|
+
description: "Page capabilities",
|
|
663
|
+
countLabel: String(model.totals.features),
|
|
664
|
+
content: projectJsonString(data.features),
|
|
665
|
+
icon: FileText
|
|
666
|
+
},
|
|
667
|
+
{
|
|
668
|
+
id: "requests",
|
|
669
|
+
name: ".anlyx/project/requests.json",
|
|
670
|
+
description: "Frontend and API requests",
|
|
671
|
+
countLabel: String(model.totals.requests),
|
|
672
|
+
content: projectJsonString(data.requests),
|
|
673
|
+
icon: Network
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
id: "flows",
|
|
677
|
+
name: ".anlyx/project/flows.json",
|
|
678
|
+
description: "Request-to-backend paths",
|
|
679
|
+
countLabel: String(model.totals.flows),
|
|
680
|
+
content: projectJsonString(data.flows),
|
|
681
|
+
icon: Workflow
|
|
682
|
+
},
|
|
683
|
+
{
|
|
684
|
+
id: "architecture",
|
|
685
|
+
name: ".anlyx/project/architecture.json",
|
|
686
|
+
description: "Architecture nodes and edges",
|
|
687
|
+
countLabel: String(model.totals.architectureNodes),
|
|
688
|
+
content: projectJsonString(data.architecture),
|
|
689
|
+
icon: Layers3
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
id: "evidence",
|
|
693
|
+
name: ".anlyx/project/evidence.json",
|
|
694
|
+
description: "Proof and confidence sources",
|
|
695
|
+
countLabel: String(model.totals.evidence),
|
|
696
|
+
content: projectJsonString(data.evidence),
|
|
697
|
+
icon: ShieldCheck
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
id: "dictionary",
|
|
701
|
+
name: ".anlyx/project/dictionary.json",
|
|
702
|
+
description: "Language and term dictionary",
|
|
703
|
+
countLabel: String(data.dictionary.terms.length),
|
|
704
|
+
content: projectJsonString(data.dictionary),
|
|
705
|
+
icon: Languages
|
|
706
|
+
}
|
|
707
|
+
];
|
|
708
|
+
if (data.measurements.length > 0) {
|
|
709
|
+
files.push({
|
|
710
|
+
id: "measurements",
|
|
711
|
+
name: ".anlyx/project/measurements.json",
|
|
712
|
+
description: "Optional runtime measurements",
|
|
713
|
+
countLabel: String(model.totals.measurements),
|
|
714
|
+
content: projectJsonString(data.measurements),
|
|
715
|
+
icon: Clock3
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
return files;
|
|
719
|
+
}
|
|
720
|
+
function projectJsonInventoryItems(data, model) {
|
|
721
|
+
return [
|
|
722
|
+
{ id: "schemaVersion", label: "schemaVersion", value: data.schemaVersion, icon: Braces },
|
|
723
|
+
{ id: "project", label: "project", value: data.project.name, icon: Folder },
|
|
724
|
+
{ id: "areas", label: "areas", value: String(model.totals.areas), icon: BriefcaseBusiness },
|
|
725
|
+
{ id: "pages", label: "pages", value: String(model.totals.pages), icon: BookOpen },
|
|
726
|
+
{ id: "features", label: "features", value: String(model.totals.features), icon: FileText },
|
|
727
|
+
{ id: "requests", label: "requests", value: String(model.totals.requests), icon: Network },
|
|
728
|
+
{ id: "flows", label: "flows", value: String(model.totals.flows), icon: Workflow },
|
|
729
|
+
{
|
|
730
|
+
id: "architecture",
|
|
731
|
+
label: "architecture",
|
|
732
|
+
value: String(model.totals.architectureNodes),
|
|
733
|
+
icon: Layers3
|
|
734
|
+
},
|
|
735
|
+
{ id: "evidence", label: "evidence", value: String(model.totals.evidence), icon: ShieldCheck },
|
|
736
|
+
{
|
|
737
|
+
id: "measurements",
|
|
738
|
+
label: "measurements",
|
|
739
|
+
value: String(model.totals.measurements),
|
|
740
|
+
icon: Clock3
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
id: "dictionary",
|
|
744
|
+
label: "dictionary",
|
|
745
|
+
value: String(data.dictionary.terms.length),
|
|
746
|
+
icon: Languages
|
|
747
|
+
}
|
|
748
|
+
];
|
|
749
|
+
}
|
|
750
|
+
function projectJsonString(value) {
|
|
751
|
+
return JSON.stringify(value, null, 2);
|
|
752
|
+
}
|
|
753
|
+
function JsonDetailsCard({ children, title }) {
|
|
754
|
+
return (_jsxs("section", { className: "project-json-details-card", children: [_jsx("h2", { children: title }), children] }));
|
|
755
|
+
}
|
|
756
|
+
function ProjectDetailRow({ label, tone, value }) {
|
|
757
|
+
return (_jsxs("div", { className: "project-detail-row", children: [_jsx("dt", { children: label }), _jsx("dd", { className: tone === "green" ? "is-green" : "", children: value })] }));
|
|
758
|
+
}
|
|
759
|
+
function ProjectStatusBar({ locale, model }) {
|
|
760
|
+
const sourceFile = projectSourceFile(model);
|
|
761
|
+
const generatedBy = projectAgentName(model);
|
|
762
|
+
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: [_jsxs("strong", { children: [model.totals.pages, " ", tp(locale, "pagesAnalyzed")] }), _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")] })] })] }));
|
|
763
|
+
}
|
|
764
|
+
function evidenceStatusLabel(status) {
|
|
765
|
+
if (status === "source-matched")
|
|
766
|
+
return "Source-matched";
|
|
767
|
+
if (status === "agent-inferred")
|
|
768
|
+
return "Agent-inferred";
|
|
769
|
+
if (status === "not-proven")
|
|
770
|
+
return "Not-proven";
|
|
771
|
+
return status.charAt(0).toUpperCase() + status.slice(1);
|
|
772
|
+
}
|
|
773
|
+
function downloadProjectJson(rawJson, fileName = "anlyx.project.json") {
|
|
774
|
+
const blob = new Blob([rawJson], { type: "application/json" });
|
|
775
|
+
const url = URL.createObjectURL(blob);
|
|
776
|
+
const link = document.createElement("a");
|
|
777
|
+
const safeFileName = fileName.split("/").pop() ?? "anlyx.project.json";
|
|
778
|
+
link.href = url;
|
|
779
|
+
link.download = safeFileName;
|
|
780
|
+
link.click();
|
|
781
|
+
URL.revokeObjectURL(url);
|
|
782
|
+
}
|
|
783
|
+
function WorkspaceSidebar({ activeView, locale, onLocaleChange, onViewChange }) {
|
|
784
|
+
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" })] }) })] }));
|
|
785
|
+
}
|
|
786
|
+
function WorkspaceHeader({ activeTab, data, onTabChange, record, recordCount }) {
|
|
787
|
+
const locale = useWorkspaceLocale();
|
|
788
|
+
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
|
|
789
|
+
? t(locale, "matchedSubtitle")
|
|
790
|
+
: `${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 }) })] }));
|
|
791
|
+
}
|
|
792
|
+
function FlowTabs({ activeTab, onTabChange }) {
|
|
793
|
+
const locale = useWorkspaceLocale();
|
|
794
|
+
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))) }));
|
|
795
|
+
}
|
|
796
|
+
function JsonReaderView({ data }) {
|
|
797
|
+
const [activeTab, setActiveTab] = useState("overview");
|
|
798
|
+
const locale = useWorkspaceLocale();
|
|
799
|
+
const flows = data.flows;
|
|
800
|
+
const endpointsById = useMemo(() => new Map(data.endpoints.map((endpoint) => [endpoint.id, endpoint])), [data.endpoints]);
|
|
801
|
+
const rawJson = useMemo(() => JSON.stringify(data, null, 2), [data]);
|
|
802
|
+
const stats = useMemo(() => jsonStats(data), [data]);
|
|
803
|
+
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 })] }))] })] }));
|
|
804
|
+
}
|
|
805
|
+
function JsonStatCard({ detail, label, value }) {
|
|
806
|
+
return (_jsxs("article", { className: "json-stat-card", children: [_jsx("span", { children: label }), _jsx("strong", { title: value, children: value }), _jsx("small", { children: detail })] }));
|
|
807
|
+
}
|
|
808
|
+
function JsonOverview({ data, stats }) {
|
|
809
|
+
const topMethods = methodCounts(data);
|
|
810
|
+
const topWarnings = data.warnings.slice(0, 4);
|
|
811
|
+
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: {
|
|
812
|
+
width: `${Math.max(12, (count / Math.max(stats.endpointCount, 1)) * 100)}%`
|
|
813
|
+
} }), _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) => {
|
|
814
|
+
const endpoint = data.endpoints.find((item) => item.id === flow.endpointId);
|
|
815
|
+
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));
|
|
816
|
+
}) })] }), _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." })) })] })] }));
|
|
817
|
+
}
|
|
818
|
+
function JsonFlowCard({ endpoint, flow }) {
|
|
819
|
+
const mainNodes = flow.mainPath
|
|
820
|
+
.map((nodeId) => flow.nodes.find((node) => node.id === nodeId))
|
|
821
|
+
.filter((node) => Boolean(node));
|
|
822
|
+
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))) })] }));
|
|
823
|
+
}
|
|
824
|
+
function jsonStats(data) {
|
|
825
|
+
return {
|
|
826
|
+
edgeCount: data.flows.reduce((sum, flow) => sum + flow.edges.length, 0),
|
|
827
|
+
endpointCount: data.endpoints.length,
|
|
828
|
+
evidenceCount: data.flows.reduce((sum, flow) => sum +
|
|
829
|
+
flow.nodes.reduce((nodeSum, node) => nodeSum + (node.evidence?.length ?? node.evidenceIds?.length ?? 0), 0), 0),
|
|
830
|
+
flowCount: data.flows.length,
|
|
831
|
+
nodeCount: data.flows.reduce((sum, flow) => sum + flow.nodes.length, 0),
|
|
832
|
+
pageCount: data.pages.length,
|
|
833
|
+
warningCount: data.warnings.length
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
function methodCounts(data) {
|
|
837
|
+
const counts = new Map();
|
|
838
|
+
for (const endpoint of data.endpoints) {
|
|
839
|
+
counts.set(endpoint.method, (counts.get(endpoint.method) ?? 0) + 1);
|
|
840
|
+
}
|
|
841
|
+
return [...counts.entries()].sort((first, second) => second[1] - first[1]);
|
|
842
|
+
}
|
|
843
|
+
function jsonTabLabel(tab) {
|
|
844
|
+
if (tab === "overview")
|
|
845
|
+
return "Overview";
|
|
846
|
+
if (tab === "flows")
|
|
847
|
+
return "Flows";
|
|
848
|
+
return "Raw JSON";
|
|
849
|
+
}
|
|
850
|
+
function formatBytes(bytes) {
|
|
851
|
+
if (bytes < 1024)
|
|
852
|
+
return `${bytes} B`;
|
|
853
|
+
if (bytes < 1024 * 1024)
|
|
854
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
855
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
856
|
+
}
|
|
857
|
+
function RequestContextPanel({ data, pageContext, record }) {
|
|
858
|
+
const locale = useWorkspaceLocale();
|
|
859
|
+
const layers = record ? diagramLayers(record) : [];
|
|
860
|
+
const controller = findFirstLayer(layers, ["controller"]);
|
|
861
|
+
const result = findFirstLayer(layers, ["result"]);
|
|
862
|
+
const pageUrl = displayPageUrl(record, locale, pageContext);
|
|
863
|
+
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
|
|
864
|
+
? requestContextDescription(record, locale)
|
|
865
|
+
: 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] }));
|
|
866
|
+
}
|
|
867
|
+
function WorkspaceContent({ record, tab }) {
|
|
868
|
+
if (tab === "summary") {
|
|
869
|
+
return _jsx(SummaryFlowView, { record: record });
|
|
870
|
+
}
|
|
871
|
+
if (tab === "diagram") {
|
|
872
|
+
return _jsx(DiagramFlowView, { record: record });
|
|
873
|
+
}
|
|
874
|
+
return _jsx(TimingWaterfallView, { record: record });
|
|
875
|
+
}
|
|
876
|
+
function TimingWaterfallView({ record }) {
|
|
877
|
+
const locale = useWorkspaceLocale();
|
|
878
|
+
const [timelineZoom, setTimelineZoom] = useState(1);
|
|
879
|
+
const [focusSlowest, setFocusSlowest] = useState(false);
|
|
880
|
+
const rows = compactTimingLayers(record);
|
|
881
|
+
const total = recordTotalDuration(record, rows);
|
|
882
|
+
const ticks = timelineTicks(total);
|
|
883
|
+
const slowestLayer = rows
|
|
884
|
+
.filter((layer) => !isUnprovenLayer(layer) && layer.type !== "api" && layer.type !== "result")
|
|
885
|
+
.sort((a, b) => estimatedLayerDuration(b, total) - estimatedLayerDuration(a, total))[0];
|
|
886
|
+
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, {})] }));
|
|
887
|
+
}
|
|
888
|
+
function TimingOverview({ rows, slowestLayer, total }) {
|
|
889
|
+
const locale = useWorkspaceLocale();
|
|
890
|
+
const breakdown = timingBreakdownSegments(rows, total);
|
|
891
|
+
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))) })] })] }));
|
|
892
|
+
}
|
|
893
|
+
function WaterfallRow({ layer, focused, runtimeSource, tickCount, total }) {
|
|
894
|
+
const locale = useWorkspaceLocale();
|
|
895
|
+
const Icon = layerIcons[layer.type] ?? Circle;
|
|
896
|
+
const muted = isUnprovenLayer(layer);
|
|
897
|
+
const sourceMatched = isSourceMatchedLayer(layer);
|
|
898
|
+
const visualType = visualLayerType(layer);
|
|
899
|
+
const isResultMarker = layer.type === "result";
|
|
900
|
+
const duration = layer.durationMs ?? estimatedLayerDuration(layer, total);
|
|
901
|
+
const runtimeGroupCount = backendRuntimeGroupCount(layer);
|
|
902
|
+
const left = layerOffset(layer, total);
|
|
903
|
+
const width = muted
|
|
904
|
+
? 0
|
|
905
|
+
: duration <= 0
|
|
906
|
+
? 0
|
|
907
|
+
: Math.max(isResultMarker ? 0.8 : 1.8, Math.min(100 - left, (duration / total) * 100));
|
|
908
|
+
const visualLeft = timelineX(left);
|
|
909
|
+
const visualWidth = timelineWidth(left, width);
|
|
910
|
+
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
|
|
911
|
+
? "waterfall-not-proven-label"
|
|
912
|
+
: `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] })] }));
|
|
913
|
+
}
|
|
914
|
+
function SummaryFlowView({ record }) {
|
|
915
|
+
const locale = useWorkspaceLocale();
|
|
916
|
+
const rows = summaryRows(record);
|
|
917
|
+
const matchedRows = rows.filter((layer) => !isUnprovenLayer(layer));
|
|
918
|
+
const unprovenRows = rows.filter((layer) => isUnprovenLayer(layer));
|
|
919
|
+
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") })] }) }));
|
|
920
|
+
}
|
|
921
|
+
function SummaryPathRow({ index, layer, runtimeSource }) {
|
|
922
|
+
const locale = useWorkspaceLocale();
|
|
923
|
+
const Icon = layerIcons[layer.type] ?? Circle;
|
|
924
|
+
const visualType = visualLayerType(layer);
|
|
925
|
+
const muted = isUnprovenLayer(layer);
|
|
926
|
+
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) })] }));
|
|
927
|
+
}
|
|
928
|
+
function DiagramFlowView({ record }) {
|
|
929
|
+
const locale = useWorkspaceLocale();
|
|
930
|
+
const [zoom, setZoom] = useState(0.82);
|
|
931
|
+
const nodeTypes = useMemo(() => ({ anlyxNode: LayeredFlowNode }), []);
|
|
932
|
+
const edgeTypes = useMemo(() => ({ anlyxSmooth: LayeredSmoothEdge }), []);
|
|
933
|
+
const model = useMemo(() => buildLayeredReactFlowDiagram(record, locale), [record, locale]);
|
|
934
|
+
useEffect(() => {
|
|
935
|
+
setZoom(0.82);
|
|
936
|
+
}, [record.id]);
|
|
937
|
+
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: {
|
|
938
|
+
"--diagram-stage-height": `${Math.ceil(model.canvasHeight * zoom + 72)}px`
|
|
939
|
+
}, children: _jsxs("div", { className: `layered-diagram layered-diagram--react-flow ${zoom === 0.82 ? "layered-diagram--fit" : ""}`, style: {
|
|
940
|
+
"--layered-column-height": `${model.canvasHeight - 50}px`,
|
|
941
|
+
transform: `scale(${zoom})`
|
|
942
|
+
}, 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"] })] })] }));
|
|
943
|
+
}
|
|
944
|
+
const layeredDiagramLayers = [
|
|
945
|
+
"browser",
|
|
946
|
+
"api",
|
|
947
|
+
"application",
|
|
948
|
+
"data",
|
|
949
|
+
"response"
|
|
950
|
+
];
|
|
951
|
+
const layeredLayout = {
|
|
952
|
+
canvasHeight: 610,
|
|
953
|
+
columnWidth: 220,
|
|
954
|
+
nodeWidth: 180,
|
|
955
|
+
nodeHeight: 148,
|
|
956
|
+
mainStartY: 98,
|
|
957
|
+
mainGap: 172,
|
|
958
|
+
secondaryStartY: 430,
|
|
959
|
+
secondaryGap: 158,
|
|
960
|
+
layerX: {
|
|
961
|
+
browser: 0,
|
|
962
|
+
api: 260,
|
|
963
|
+
application: 520,
|
|
964
|
+
data: 860,
|
|
965
|
+
response: 1120
|
|
966
|
+
}
|
|
967
|
+
};
|
|
968
|
+
function LayeredDiagramColumnBackdrop({ hasSecondaryNodes, layer, locale }) {
|
|
969
|
+
const heading = layeredLayerMeta(layer, locale);
|
|
970
|
+
const Icon = heading.icon;
|
|
971
|
+
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] }));
|
|
972
|
+
}
|
|
973
|
+
function LayeredFlowNode({ data }) {
|
|
974
|
+
return _jsx(LayeredNodeCard, { node: data.node });
|
|
975
|
+
}
|
|
976
|
+
function LayeredNodeCard({ node }) {
|
|
977
|
+
const Icon = layeredNodeIcon(node);
|
|
978
|
+
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) })] })] }));
|
|
979
|
+
}
|
|
980
|
+
function LayeredSmoothEdge({ data, sourcePosition, sourceX, sourceY, targetPosition, targetX, targetY }) {
|
|
981
|
+
const [path] = getSmoothStepPath({
|
|
982
|
+
borderRadius: 18,
|
|
983
|
+
offset: 26,
|
|
984
|
+
sourcePosition,
|
|
985
|
+
sourceX,
|
|
986
|
+
sourceY,
|
|
987
|
+
targetPosition,
|
|
988
|
+
targetX,
|
|
989
|
+
targetY
|
|
990
|
+
});
|
|
991
|
+
const kind = data?.kind ?? "executed";
|
|
992
|
+
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 })] }));
|
|
993
|
+
}
|
|
994
|
+
function buildLayeredReactFlowDiagram(record, locale) {
|
|
995
|
+
const diagramNodes = assignLayeredNodePositions(layeredDiagramLayersForRecord(record).map((layer) => toLayeredDiagramNode(layer, record, locale)));
|
|
996
|
+
const diagramEdges = buildLayeredDiagramEdges(diagramNodes);
|
|
997
|
+
return {
|
|
998
|
+
layers: layeredDiagramLayers,
|
|
999
|
+
canvasHeight: diagramCanvasHeight(diagramNodes),
|
|
1000
|
+
nodes: diagramNodes.map((node) => ({
|
|
1001
|
+
id: node.id,
|
|
1002
|
+
type: "anlyxNode",
|
|
1003
|
+
position: { x: node.x, y: node.y },
|
|
1004
|
+
data: { node },
|
|
1005
|
+
draggable: false,
|
|
1006
|
+
selectable: false
|
|
1007
|
+
})),
|
|
1008
|
+
edges: diagramEdges.map((edge) => {
|
|
1009
|
+
const from = diagramNodes.find((node) => node.id === edge.from);
|
|
1010
|
+
const to = diagramNodes.find((node) => node.id === edge.to);
|
|
1011
|
+
const handles = from && to ? reactFlowHandlesForEdge(from, to) : {};
|
|
1012
|
+
return {
|
|
1013
|
+
id: edge.id,
|
|
1014
|
+
source: edge.from,
|
|
1015
|
+
target: edge.to,
|
|
1016
|
+
type: "anlyxSmooth",
|
|
1017
|
+
data: { kind: edge.kind },
|
|
1018
|
+
focusable: false,
|
|
1019
|
+
selectable: false,
|
|
1020
|
+
...handles
|
|
1021
|
+
};
|
|
1022
|
+
})
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function layeredDiagramLayersForRecord(record) {
|
|
1026
|
+
const layers = diagramLayers(record).filter((layer) => layer.type !== "page");
|
|
1027
|
+
const fallback = layers.length > 0 ? layers : timingLayers(record);
|
|
1028
|
+
const hasBlocked = hasBlockedOutcome(record);
|
|
1029
|
+
const order = hasBlocked
|
|
1030
|
+
? [
|
|
1031
|
+
"action",
|
|
1032
|
+
"api",
|
|
1033
|
+
"controller",
|
|
1034
|
+
"auth",
|
|
1035
|
+
"decision",
|
|
1036
|
+
"result",
|
|
1037
|
+
"service",
|
|
1038
|
+
"repository",
|
|
1039
|
+
"database"
|
|
1040
|
+
]
|
|
1041
|
+
: [
|
|
1042
|
+
"action",
|
|
1043
|
+
"api",
|
|
1044
|
+
"controller",
|
|
1045
|
+
"auth",
|
|
1046
|
+
"decision",
|
|
1047
|
+
"service",
|
|
1048
|
+
"repository",
|
|
1049
|
+
"database",
|
|
1050
|
+
"result"
|
|
1051
|
+
];
|
|
1052
|
+
return order
|
|
1053
|
+
.map((type) => fallback.find((layer) => layer.type === type))
|
|
1054
|
+
.filter((layer) => Boolean(layer));
|
|
1055
|
+
}
|
|
1056
|
+
function diagramCanvasHeight(nodes) {
|
|
1057
|
+
return Math.max(layeredLayout.canvasHeight, ...nodes.map((node) => node.y + layeredLayout.nodeHeight + 110));
|
|
1058
|
+
}
|
|
1059
|
+
function assignLayeredNodePositions(nodes) {
|
|
1060
|
+
const mainSlotByLayer = new Map();
|
|
1061
|
+
const secondarySlotByLayer = new Map();
|
|
1062
|
+
return nodes.map((node) => {
|
|
1063
|
+
const slotMap = node.isMainPath ? mainSlotByLayer : secondarySlotByLayer;
|
|
1064
|
+
const slot = slotMap.get(node.layer) ?? 0;
|
|
1065
|
+
const y = node.isMainPath
|
|
1066
|
+
? layeredLayout.mainStartY + slot * layeredLayout.mainGap
|
|
1067
|
+
: layeredLayout.secondaryStartY + slot * layeredLayout.secondaryGap;
|
|
1068
|
+
slotMap.set(node.layer, slot + 1);
|
|
1069
|
+
return {
|
|
1070
|
+
...node,
|
|
1071
|
+
slot,
|
|
1072
|
+
x: layeredLayout.layerX[node.layer] +
|
|
1073
|
+
(layeredLayout.columnWidth - layeredLayout.nodeWidth) / 2,
|
|
1074
|
+
y
|
|
1075
|
+
};
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
function toLayeredDiagramNode(layer, record, locale) {
|
|
1079
|
+
const status = layeredDiagramNodeStatus(layer, record);
|
|
1080
|
+
return {
|
|
1081
|
+
id: layer.id,
|
|
1082
|
+
sourceLayer: layer,
|
|
1083
|
+
layer: diagramLayerForFlowLayer(layer.type),
|
|
1084
|
+
kind: layer.type,
|
|
1085
|
+
title: layeredDiagramTitle(layer, record, locale),
|
|
1086
|
+
subtitle: layeredDiagramSubtitle(layer, record),
|
|
1087
|
+
status,
|
|
1088
|
+
edgeKind: edgeKindForLayer(layer, status),
|
|
1089
|
+
isMainPath: !isUnprovenLayer(layer),
|
|
1090
|
+
slot: 0,
|
|
1091
|
+
x: 0,
|
|
1092
|
+
y: 0,
|
|
1093
|
+
...(layer.durationMs !== undefined ? { durationMs: Math.round(layer.durationMs) } : {})
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function buildLayeredDiagramEdges(nodes) {
|
|
1097
|
+
const mainNodes = nodes.filter((node) => node.isMainPath).sort(compareLayeredNodes);
|
|
1098
|
+
const secondaryNodes = nodes.filter((node) => !node.isMainPath).sort(compareLayeredNodes);
|
|
1099
|
+
const edges = [];
|
|
1100
|
+
for (let index = 0; index < mainNodes.length - 1; index += 1) {
|
|
1101
|
+
const from = mainNodes[index];
|
|
1102
|
+
const to = mainNodes[index + 1];
|
|
1103
|
+
if (from && to) {
|
|
1104
|
+
edges.push({
|
|
1105
|
+
id: `${from.id}->${to.id}`,
|
|
1106
|
+
from: from.id,
|
|
1107
|
+
to: to.id,
|
|
1108
|
+
kind: edgeKindBetween(from, to)
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const anchor = mainNodes.find((node) => node.layer === "application" && node.kind !== "controller") ??
|
|
1113
|
+
mainNodes.find((node) => node.layer === "application") ??
|
|
1114
|
+
mainNodes.find((node) => node.layer === "api");
|
|
1115
|
+
if (anchor) {
|
|
1116
|
+
for (const node of secondaryNodes) {
|
|
1117
|
+
edges.push({
|
|
1118
|
+
id: `${anchor.id}->${node.id}`,
|
|
1119
|
+
from: anchor.id,
|
|
1120
|
+
to: node.id,
|
|
1121
|
+
kind: "not_proven"
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return edges;
|
|
1126
|
+
}
|
|
1127
|
+
function compareLayeredNodes(first, second) {
|
|
1128
|
+
const layerDelta = layeredDiagramLayers.indexOf(first.layer) - layeredDiagramLayers.indexOf(second.layer);
|
|
1129
|
+
if (layerDelta !== 0)
|
|
1130
|
+
return layerDelta;
|
|
1131
|
+
return layerOrderIndex(first.kind) - layerOrderIndex(second.kind);
|
|
1132
|
+
}
|
|
1133
|
+
function reactFlowHandlesForEdge(from, to) {
|
|
1134
|
+
if (from.layer === to.layer) {
|
|
1135
|
+
return to.y > from.y
|
|
1136
|
+
? { sourceHandle: "bottom", targetHandle: "top" }
|
|
1137
|
+
: { sourceHandle: "top", targetHandle: "bottom" };
|
|
1138
|
+
}
|
|
1139
|
+
return { sourceHandle: "right", targetHandle: "left" };
|
|
1140
|
+
}
|
|
1141
|
+
function edgeKindBetween(from, to) {
|
|
1142
|
+
if (from.status === "blocked" || to.status === "blocked")
|
|
1143
|
+
return "blocked";
|
|
1144
|
+
if (from.status === "inferred" || to.status === "inferred")
|
|
1145
|
+
return "inferred";
|
|
1146
|
+
if (from.status === "source_matched" || to.status === "source_matched")
|
|
1147
|
+
return "source_matched";
|
|
1148
|
+
return "executed";
|
|
1149
|
+
}
|
|
1150
|
+
function edgeKindForLayer(layer, status) {
|
|
1151
|
+
if (status === "blocked")
|
|
1152
|
+
return "blocked";
|
|
1153
|
+
if (status === "inferred")
|
|
1154
|
+
return "inferred";
|
|
1155
|
+
if (isUnprovenLayer(layer) || status === "not_proven")
|
|
1156
|
+
return "not_proven";
|
|
1157
|
+
if (layer.type !== "action" && layer.type !== "api" && layer.type !== "result") {
|
|
1158
|
+
return "source_matched";
|
|
1159
|
+
}
|
|
1160
|
+
return "executed";
|
|
1161
|
+
}
|
|
1162
|
+
function diagramLayerForFlowLayer(layer) {
|
|
1163
|
+
if (layer === "action")
|
|
1164
|
+
return "browser";
|
|
1165
|
+
if (layer === "api")
|
|
1166
|
+
return "api";
|
|
1167
|
+
if (layer === "repository" || layer === "database")
|
|
1168
|
+
return "data";
|
|
1169
|
+
if (layer === "result")
|
|
1170
|
+
return "response";
|
|
1171
|
+
return "application";
|
|
1172
|
+
}
|
|
1173
|
+
function layeredDiagramNodeStatus(layer, record) {
|
|
1174
|
+
if (isUnprovenLayer(layer))
|
|
1175
|
+
return "not_proven";
|
|
1176
|
+
if (layer.execution === "blocked")
|
|
1177
|
+
return "blocked";
|
|
1178
|
+
if (layer.execution === "inferred" || layer.evidenceLevel === "inferred")
|
|
1179
|
+
return "inferred";
|
|
1180
|
+
if (layer.type === "result" && outcomeTone(record) === "success")
|
|
1181
|
+
return "success";
|
|
1182
|
+
if (layer.type === "action" || layer.type === "api")
|
|
1183
|
+
return "observed";
|
|
1184
|
+
if (layer.type !== "result")
|
|
1185
|
+
return "source_matched";
|
|
1186
|
+
return "matched";
|
|
1187
|
+
}
|
|
1188
|
+
function layeredDiagramTitle(layer, record, locale) {
|
|
1189
|
+
if (layer.type === "action")
|
|
1190
|
+
return locale === "ko" ? "사용자 액션" : "User Action";
|
|
1191
|
+
if (layer.type === "api")
|
|
1192
|
+
return locale === "ko" ? "HTTP 요청" : "HTTP Request";
|
|
1193
|
+
if (layer.type === "result")
|
|
1194
|
+
return outcomeTone(record) === "success" ? "OK" : outcomeLabel(record, locale);
|
|
1195
|
+
return layerLabel(layer, locale);
|
|
1196
|
+
}
|
|
1197
|
+
function layeredDiagramSubtitle(layer, record) {
|
|
1198
|
+
if (layer.type === "action")
|
|
1199
|
+
return record.action?.label ?? layer.label;
|
|
1200
|
+
if (layer.type === "api")
|
|
1201
|
+
return `${record.method} ${record.path}`;
|
|
1202
|
+
if (layer.type === "result")
|
|
1203
|
+
return layer.label;
|
|
1204
|
+
return layer.label;
|
|
1205
|
+
}
|
|
1206
|
+
function layeredLayerMeta(layer, locale) {
|
|
1207
|
+
const ko = locale === "ko";
|
|
1208
|
+
if (layer === "browser") {
|
|
1209
|
+
return {
|
|
1210
|
+
icon: MousePointerClick,
|
|
1211
|
+
title: ko ? "브라우저" : "Browser",
|
|
1212
|
+
subtitle: ko ? "사용자 상호작용" : "User interaction"
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
if (layer === "api") {
|
|
1216
|
+
return {
|
|
1217
|
+
icon: Network,
|
|
1218
|
+
title: "API",
|
|
1219
|
+
subtitle: ko ? "요청과 라우팅" : "Edge & routing"
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
if (layer === "application") {
|
|
1223
|
+
return {
|
|
1224
|
+
icon: Workflow,
|
|
1225
|
+
title: ko ? "애플리케이션" : "Application",
|
|
1226
|
+
subtitle: ko ? "비즈니스 로직" : "Business logic"
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
if (layer === "data") {
|
|
1230
|
+
return {
|
|
1231
|
+
icon: Database,
|
|
1232
|
+
title: ko ? "데이터" : "Data",
|
|
1233
|
+
subtitle: ko ? "저장소 계층" : "Persistence layer"
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
return {
|
|
1237
|
+
icon: Flag,
|
|
1238
|
+
title: ko ? "응답" : "Response",
|
|
1239
|
+
subtitle: ko ? "클라이언트로 반환" : "Back to client"
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
function layeredNodeIcon(node) {
|
|
1243
|
+
if (node.kind === "action")
|
|
1244
|
+
return MousePointerClick;
|
|
1245
|
+
if (node.kind === "api")
|
|
1246
|
+
return Network;
|
|
1247
|
+
if (node.kind === "auth" || node.kind === "decision")
|
|
1248
|
+
return Lock;
|
|
1249
|
+
if (node.kind === "service")
|
|
1250
|
+
return Layers3;
|
|
1251
|
+
if (node.kind === "repository")
|
|
1252
|
+
return Box;
|
|
1253
|
+
if (node.kind === "database")
|
|
1254
|
+
return Database;
|
|
1255
|
+
if (node.kind === "result")
|
|
1256
|
+
return Flag;
|
|
1257
|
+
if (node.kind === "controller")
|
|
1258
|
+
return FileCode2;
|
|
1259
|
+
return Circle;
|
|
1260
|
+
}
|
|
1261
|
+
function layeredDiagramNodeStatusLabel(status) {
|
|
1262
|
+
if (status === "observed")
|
|
1263
|
+
return "observed";
|
|
1264
|
+
if (status === "source_matched")
|
|
1265
|
+
return "source matched";
|
|
1266
|
+
if (status === "inferred")
|
|
1267
|
+
return "inferred";
|
|
1268
|
+
if (status === "not_proven")
|
|
1269
|
+
return "not proven";
|
|
1270
|
+
if (status === "success")
|
|
1271
|
+
return "success";
|
|
1272
|
+
if (status === "blocked")
|
|
1273
|
+
return "blocked";
|
|
1274
|
+
return "matched";
|
|
1275
|
+
}
|
|
1276
|
+
function recordTotalDuration(record, rows) {
|
|
1277
|
+
const baseDuration = record.durationMs ?? record.duration ?? 0;
|
|
1278
|
+
const layerEnd = Math.max(0, ...(rows ?? timingLayers(record)).map((layer) => (layer.startOffsetMs ?? 0) + Math.max(1, layer.durationMs ?? 0)));
|
|
1279
|
+
return Math.max(1, Math.round(Math.max(baseDuration, layerEnd)));
|
|
1280
|
+
}
|
|
1281
|
+
function recordDuration(record) {
|
|
1282
|
+
return Math.round(recordTotalDuration(record));
|
|
1283
|
+
}
|
|
1284
|
+
function FlowInspector({ onSelect, pageContext, record, records, selectedId }) {
|
|
1285
|
+
const locale = useWorkspaceLocale();
|
|
1286
|
+
const evidence = useMemo(() => record?.evidence ?? [], [record]);
|
|
1287
|
+
const recentRecords = useMemo(() => mergeRecentRecords(records, selectedId), [records, selectedId]);
|
|
1288
|
+
const coverage = record
|
|
1289
|
+
? evidenceCoverage(record)
|
|
1290
|
+
: { browser: 0, backend: 0, source: 0, notProven: 0 };
|
|
1291
|
+
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") }) }))] }));
|
|
1292
|
+
}
|
|
1293
|
+
function RecentEventsList({ onSelect, records, selectedId }) {
|
|
1294
|
+
const locale = useWorkspaceLocale();
|
|
1295
|
+
const primaryRecords = records.filter(isPrimaryRecord).slice(0, 5);
|
|
1296
|
+
const backgroundRecords = records.filter((record) => !isPrimaryRecord(record)).slice(0, 4);
|
|
1297
|
+
if (records.length === 0) {
|
|
1298
|
+
return _jsx("p", { className: "events-empty", children: t(locale, "noCurrentPageRequestsCopy") });
|
|
1299
|
+
}
|
|
1300
|
+
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] }));
|
|
1301
|
+
}
|
|
1302
|
+
function RecentEventButton({ background = false, onSelect, record, selectedId }) {
|
|
1303
|
+
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
|
|
1304
|
+
? ` · ${record.occurrenceCount}x`
|
|
1305
|
+
: ""] })] }));
|
|
1306
|
+
}
|
|
1307
|
+
function EmptyWorkspace({ pageContext }) {
|
|
1308
|
+
const locale = useWorkspaceLocale();
|
|
1309
|
+
const pageUrl = pageContext ? normalizeDisplayUrl(pageContext.pageUrl) : undefined;
|
|
1310
|
+
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
|
|
1311
|
+
? `${pageUrl}: ${t(locale, "noCurrentPageRequestsCopy")}`
|
|
1312
|
+
: t(locale, "emptyWorkspaceCopy") })] }));
|
|
1313
|
+
}
|
|
1314
|
+
function InspectorSection({ children, count, title }) {
|
|
1315
|
+
return (_jsxs("section", { className: "inspector-section", children: [_jsxs("h3", { children: [title, count ? _jsx("span", { children: count }) : null] }), children] }));
|
|
1316
|
+
}
|
|
1317
|
+
function DurationLegend() {
|
|
1318
|
+
const locale = useWorkspaceLocale();
|
|
1319
|
+
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")] })] }));
|
|
1320
|
+
}
|
|
1321
|
+
function ConfidenceBars() {
|
|
1322
|
+
return (_jsxs("span", { className: "confidence-bars", "aria-hidden": "true", children: [_jsx("i", {}), _jsx("i", {}), _jsx("i", {})] }));
|
|
1323
|
+
}
|
|
1324
|
+
function parseFlowRecord(value) {
|
|
1325
|
+
try {
|
|
1326
|
+
const parsed = JSON.parse(value);
|
|
1327
|
+
if (typeof parsed.id === "string" &&
|
|
1328
|
+
typeof parsed.requestId === "string" &&
|
|
1329
|
+
typeof parsed.method === "string" &&
|
|
1330
|
+
typeof parsed.path === "string" &&
|
|
1331
|
+
(parsed.pageUrl === undefined || typeof parsed.pageUrl === "string") &&
|
|
1332
|
+
Array.isArray(parsed.layers)) {
|
|
1333
|
+
return {
|
|
1334
|
+
...parsed,
|
|
1335
|
+
trigger: parsed.trigger === "user_action" ? "user_action" : "background",
|
|
1336
|
+
priority: parsed.priority === "background" ? "background" : "primary",
|
|
1337
|
+
runtimeSource: parsed.runtimeSource === "server" || parsed.runtimeSource === "browser"
|
|
1338
|
+
? parsed.runtimeSource
|
|
1339
|
+
: "unknown"
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
function parsePageContextEvent(value) {
|
|
1349
|
+
try {
|
|
1350
|
+
const parsed = JSON.parse(value);
|
|
1351
|
+
if (parsed.type === "page_context" &&
|
|
1352
|
+
typeof parsed.pageUrl === "string" &&
|
|
1353
|
+
typeof parsed.contextId === "string" &&
|
|
1354
|
+
typeof parsed.observedAt === "string") {
|
|
1355
|
+
return {
|
|
1356
|
+
type: "page_context",
|
|
1357
|
+
pageUrl: parsed.pageUrl,
|
|
1358
|
+
contextId: parsed.contextId,
|
|
1359
|
+
observedAt: parsed.observedAt
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
catch {
|
|
1364
|
+
return null;
|
|
1365
|
+
}
|
|
1366
|
+
return null;
|
|
1367
|
+
}
|
|
1368
|
+
function recordsForPageContext(records, pageContext) {
|
|
1369
|
+
if (!pageContext) {
|
|
1370
|
+
return records;
|
|
1371
|
+
}
|
|
1372
|
+
return records.filter((record) => recordBelongsToPageContext(record, pageContext));
|
|
1373
|
+
}
|
|
1374
|
+
function recordBelongsToPageContext(record, pageContext) {
|
|
1375
|
+
if (record.contextId && record.contextId === pageContext.contextId) {
|
|
1376
|
+
return true;
|
|
1377
|
+
}
|
|
1378
|
+
if (record.pageUrl) {
|
|
1379
|
+
return normalizeDisplayUrl(record.pageUrl) === normalizeDisplayUrl(pageContext.pageUrl);
|
|
1380
|
+
}
|
|
1381
|
+
return false;
|
|
1382
|
+
}
|
|
1383
|
+
function scopedRecentRecords(records, selectedRecord) {
|
|
1384
|
+
if (!selectedRecord) {
|
|
1385
|
+
return records;
|
|
1386
|
+
}
|
|
1387
|
+
const selectedAt = recordTime(selectedRecord);
|
|
1388
|
+
const windowStart = selectedAt - 1_500;
|
|
1389
|
+
const windowEnd = Math.max(Date.now(), selectedAt + 12_000);
|
|
1390
|
+
const selectedContext = selectedRecord.contextId;
|
|
1391
|
+
const scoped = records.filter((record) => {
|
|
1392
|
+
if (record.id === selectedRecord.id) {
|
|
1393
|
+
return true;
|
|
1394
|
+
}
|
|
1395
|
+
const time = recordTime(record);
|
|
1396
|
+
if (time < windowStart || time > windowEnd) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
if (!selectedContext || !record.contextId) {
|
|
1400
|
+
return true;
|
|
1401
|
+
}
|
|
1402
|
+
return record.contextId === selectedContext;
|
|
1403
|
+
});
|
|
1404
|
+
return scoped.length > 0 ? scoped : [selectedRecord];
|
|
1405
|
+
}
|
|
1406
|
+
function mergeRecentRecords(records, selectedId) {
|
|
1407
|
+
const grouped = new Map();
|
|
1408
|
+
for (const record of records) {
|
|
1409
|
+
const key = [
|
|
1410
|
+
record.contextId ?? "",
|
|
1411
|
+
record.priority,
|
|
1412
|
+
record.method,
|
|
1413
|
+
normalizeComparablePath(record.path),
|
|
1414
|
+
record.status ?? "",
|
|
1415
|
+
record.matchState
|
|
1416
|
+
].join("|");
|
|
1417
|
+
grouped.set(key, [...(grouped.get(key) ?? []), record]);
|
|
1418
|
+
}
|
|
1419
|
+
return [...grouped.values()]
|
|
1420
|
+
.map((group) => {
|
|
1421
|
+
const selectedRecord = selectedId
|
|
1422
|
+
? group.find((record) => record.id === selectedId)
|
|
1423
|
+
: undefined;
|
|
1424
|
+
const latestRecord = group.reduce((latest, record) => recordTime(record) > recordTime(latest) ? record : latest);
|
|
1425
|
+
return {
|
|
1426
|
+
...(selectedRecord ?? latestRecord),
|
|
1427
|
+
occurrenceCount: group.length
|
|
1428
|
+
};
|
|
1429
|
+
})
|
|
1430
|
+
.sort((left, right) => recordTime(right) - recordTime(left));
|
|
1431
|
+
}
|
|
1432
|
+
function normalizeComparablePath(path) {
|
|
1433
|
+
try {
|
|
1434
|
+
const parsed = new URL(path, "http://anlyx.local");
|
|
1435
|
+
parsed.searchParams.sort();
|
|
1436
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
return path;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
function displayPageUrl(record, locale, pageContext) {
|
|
1443
|
+
if (pageContext?.pageUrl && (!record || isPageContextCurrentForRecord(pageContext, record))) {
|
|
1444
|
+
return normalizeDisplayUrl(pageContext.pageUrl);
|
|
1445
|
+
}
|
|
1446
|
+
if (record?.pageUrl) {
|
|
1447
|
+
return normalizeDisplayUrl(record.pageUrl);
|
|
1448
|
+
}
|
|
1449
|
+
if (record?.contextId?.startsWith("page:")) {
|
|
1450
|
+
const path = record.contextId.slice("page:".length) || "/";
|
|
1451
|
+
return normalizeDisplayUrl(path);
|
|
1452
|
+
}
|
|
1453
|
+
return t(locale, "pageUrlNotCaptured");
|
|
1454
|
+
}
|
|
1455
|
+
function isPageContextCurrentForRecord(pageContext, record) {
|
|
1456
|
+
if (!record.pageUrl && !record.contextId) {
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
const pageObservedAt = Date.parse(pageContext.observedAt);
|
|
1460
|
+
const recordCreatedAt = Date.parse(record.createdAt);
|
|
1461
|
+
if (!Number.isFinite(pageObservedAt) || !Number.isFinite(recordCreatedAt)) {
|
|
1462
|
+
return pageContext.contextId !== record.contextId;
|
|
1463
|
+
}
|
|
1464
|
+
return pageObservedAt >= recordCreatedAt || pageContext.contextId !== record.contextId;
|
|
1465
|
+
}
|
|
1466
|
+
function normalizeDisplayUrl(urlOrPath) {
|
|
1467
|
+
try {
|
|
1468
|
+
const parsed = new URL(urlOrPath);
|
|
1469
|
+
if (parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1") {
|
|
1470
|
+
return `${parsed.pathname}${parsed.search}` || "/";
|
|
1471
|
+
}
|
|
1472
|
+
return `${parsed.origin}${parsed.pathname}${parsed.search}`;
|
|
1473
|
+
}
|
|
1474
|
+
catch {
|
|
1475
|
+
return urlOrPath || "/";
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
function isPrimaryRecord(record) {
|
|
1479
|
+
if (record.priority === "background") {
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
if (record.trigger === "user_action") {
|
|
1483
|
+
return true;
|
|
1484
|
+
}
|
|
1485
|
+
return !isPassiveImplementationRequest(record);
|
|
1486
|
+
}
|
|
1487
|
+
function isPassiveImplementationRequest(record) {
|
|
1488
|
+
const method = record.method.toUpperCase();
|
|
1489
|
+
const segments = record.path.split("?")[0]?.toLowerCase().split("/").filter(Boolean) ?? [];
|
|
1490
|
+
const last = segments[segments.length - 1] ?? "";
|
|
1491
|
+
if (segments.some((segment) => [
|
|
1492
|
+
"health",
|
|
1493
|
+
"healthz",
|
|
1494
|
+
"ready",
|
|
1495
|
+
"readyz",
|
|
1496
|
+
"live",
|
|
1497
|
+
"livez",
|
|
1498
|
+
"ping",
|
|
1499
|
+
"metrics",
|
|
1500
|
+
"poll",
|
|
1501
|
+
"polling"
|
|
1502
|
+
].includes(segment))) {
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
if (segments.some((segment) => ["page-views", "analytics", "telemetry", "events", "metrics"].includes(segment))) {
|
|
1506
|
+
return true;
|
|
1507
|
+
}
|
|
1508
|
+
if (method === "GET" && ["me", "session", "profile", "current-user"].includes(last)) {
|
|
1509
|
+
return true;
|
|
1510
|
+
}
|
|
1511
|
+
if (segments.includes("csrf") || segments.includes("xsrf")) {
|
|
1512
|
+
return true;
|
|
1513
|
+
}
|
|
1514
|
+
if (segments.includes("auth") &&
|
|
1515
|
+
["session", "refresh", "token", "csrf", "status"].includes(last)) {
|
|
1516
|
+
return true;
|
|
1517
|
+
}
|
|
1518
|
+
return (method === "GET" &&
|
|
1519
|
+
["saved-benefits", "saved-items", "bookmarks", "favorites"].some((segment) => segments.includes(segment)));
|
|
1520
|
+
}
|
|
1521
|
+
function recordTime(record) {
|
|
1522
|
+
const time = Date.parse(record.createdAt);
|
|
1523
|
+
return Number.isFinite(time) ? time : Date.now();
|
|
1524
|
+
}
|
|
1525
|
+
function orderedLayers(record) {
|
|
1526
|
+
const used = new Set();
|
|
1527
|
+
const ordered = [];
|
|
1528
|
+
for (const type of layerOrder) {
|
|
1529
|
+
for (const layer of record.layers) {
|
|
1530
|
+
if (layer.type === type && !used.has(layer.id)) {
|
|
1531
|
+
used.add(layer.id);
|
|
1532
|
+
ordered.push(layer);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
for (const layer of record.layers) {
|
|
1537
|
+
if (!used.has(layer.id)) {
|
|
1538
|
+
ordered.push(layer);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return ordered;
|
|
1542
|
+
}
|
|
1543
|
+
function timingLayers(record) {
|
|
1544
|
+
if (!record.backendSpans?.length) {
|
|
1545
|
+
return assignReadableOffsets(orderedLayers(record).filter((layer) => layer.type !== "page"), Math.max(record.durationMs ?? record.duration ?? 1, 1));
|
|
1546
|
+
}
|
|
1547
|
+
const action = record.layers.find((layer) => layer.type === "action");
|
|
1548
|
+
const api = record.layers.find((layer) => layer.type === "api");
|
|
1549
|
+
const result = record.layers.find((layer) => layer.type === "result");
|
|
1550
|
+
const total = Math.max(record.durationMs ?? record.duration ?? 1, 1);
|
|
1551
|
+
const layers = [];
|
|
1552
|
+
if (action) {
|
|
1553
|
+
layers.push({
|
|
1554
|
+
...action,
|
|
1555
|
+
startOffsetMs: 0,
|
|
1556
|
+
durationMs: Math.min(action.durationMs ?? 4, 4)
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
if (api) {
|
|
1560
|
+
layers.push({
|
|
1561
|
+
...api,
|
|
1562
|
+
startOffsetMs: 3,
|
|
1563
|
+
durationMs: Math.max(1, (record.durationMs ?? record.duration ?? api.durationMs ?? 1) - 3)
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
layers.push(...[...record.backendSpans]
|
|
1567
|
+
.filter((span) => span.type !== "api")
|
|
1568
|
+
.sort(compareBackendSpans)
|
|
1569
|
+
.map(backendSpanToLayer));
|
|
1570
|
+
if (result) {
|
|
1571
|
+
layers.push({
|
|
1572
|
+
...result,
|
|
1573
|
+
startOffsetMs: Math.max(0, total - Math.max(1, result.durationMs ?? 1)),
|
|
1574
|
+
durationMs: Math.max(1, result.durationMs ?? 1)
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
return layers;
|
|
1578
|
+
}
|
|
1579
|
+
function compactTimingLayers(record) {
|
|
1580
|
+
if (!record.backendSpans?.length) {
|
|
1581
|
+
return timingLayers(record);
|
|
1582
|
+
}
|
|
1583
|
+
const rawRows = timingLayers(record);
|
|
1584
|
+
const action = rawRows.find((layer) => layer.type === "action");
|
|
1585
|
+
const api = rawRows.find((layer) => layer.type === "api");
|
|
1586
|
+
const result = rawRows.find((layer) => layer.type === "result");
|
|
1587
|
+
const observedRows = summarizeBackendObservedSpans(record.backendSpans);
|
|
1588
|
+
const requestOverhead = requestOverheadLayer(record, api, result);
|
|
1589
|
+
const sourceRows = orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
1590
|
+
const observedTypes = new Set(observedRows.map((layer) => layer.type));
|
|
1591
|
+
const unprovenRows = sourceRows.filter((layer) => isUnprovenLayer(layer) &&
|
|
1592
|
+
!observedTypes.has(layer.type) &&
|
|
1593
|
+
layer.type !== "action" &&
|
|
1594
|
+
layer.type !== "api" &&
|
|
1595
|
+
layer.type !== "result");
|
|
1596
|
+
return [action, requestOverhead, ...observedRows, result, ...unprovenRows].filter((layer) => Boolean(layer));
|
|
1597
|
+
}
|
|
1598
|
+
function summaryRows(record) {
|
|
1599
|
+
if (!record.backendSpans?.length) {
|
|
1600
|
+
return orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
1601
|
+
}
|
|
1602
|
+
const sourceRows = orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
1603
|
+
const action = sourceRows.find((layer) => layer.type === "action");
|
|
1604
|
+
const api = sourceRows.find((layer) => layer.type === "api");
|
|
1605
|
+
const result = sourceRows.find((layer) => layer.type === "result");
|
|
1606
|
+
const observedRows = summarizeBackendObservedSpans(record.backendSpans);
|
|
1607
|
+
const requestOverhead = requestOverheadLayer(record, api, result);
|
|
1608
|
+
const observedTypes = new Set(observedRows.map((layer) => layer.type));
|
|
1609
|
+
const unprovenRows = sourceRows.filter((layer) => isUnprovenLayer(layer) &&
|
|
1610
|
+
!observedTypes.has(layer.type) &&
|
|
1611
|
+
layer.type !== "action" &&
|
|
1612
|
+
layer.type !== "api" &&
|
|
1613
|
+
layer.type !== "result");
|
|
1614
|
+
return [action, requestOverhead, ...observedRows, result, ...unprovenRows].filter((layer) => Boolean(layer));
|
|
1615
|
+
}
|
|
1616
|
+
function assignReadableOffsets(layers, total) {
|
|
1617
|
+
const backend = layers.filter((layer) => layer.type !== "action" &&
|
|
1618
|
+
layer.type !== "api" &&
|
|
1619
|
+
layer.type !== "result" &&
|
|
1620
|
+
!isUnprovenLayer(layer));
|
|
1621
|
+
const backendStart = Math.max(14, Math.round(total * 0.08));
|
|
1622
|
+
const backendEnd = Math.max(backendStart + 1, Math.round(total * 0.78));
|
|
1623
|
+
const slot = backend.length > 0 ? Math.max(8, Math.round((backendEnd - backendStart) / backend.length)) : 0;
|
|
1624
|
+
return layers.map((layer) => {
|
|
1625
|
+
if (layer.startOffsetMs !== undefined)
|
|
1626
|
+
return layer;
|
|
1627
|
+
if (layer.type === "action") {
|
|
1628
|
+
return { ...layer, startOffsetMs: 0, durationMs: Math.min(layer.durationMs ?? 4, 4) };
|
|
1629
|
+
}
|
|
1630
|
+
if (layer.type === "api") {
|
|
1631
|
+
return {
|
|
1632
|
+
...layer,
|
|
1633
|
+
startOffsetMs: 3,
|
|
1634
|
+
durationMs: Math.max(1, total - 3)
|
|
1635
|
+
};
|
|
1636
|
+
}
|
|
1637
|
+
if (layer.type === "result") {
|
|
1638
|
+
return {
|
|
1639
|
+
...layer,
|
|
1640
|
+
startOffsetMs: Math.max(0, total - Math.max(1, layer.durationMs ?? 2)),
|
|
1641
|
+
durationMs: Math.max(1, layer.durationMs ?? 2)
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
if (isUnprovenLayer(layer)) {
|
|
1645
|
+
return layer;
|
|
1646
|
+
}
|
|
1647
|
+
const index = Math.max(0, backend.findIndex((item) => item.id === layer.id));
|
|
1648
|
+
const estimatedDuration = layer.durationMs ?? estimatedLayerDuration(layer, total);
|
|
1649
|
+
return {
|
|
1650
|
+
...layer,
|
|
1651
|
+
startOffsetMs: Math.min(Math.max(0, total - estimatedDuration), backendStart + slot * index),
|
|
1652
|
+
durationMs: estimatedDuration
|
|
1653
|
+
};
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
function backendSpanToLayer(span) {
|
|
1657
|
+
return {
|
|
1658
|
+
id: `backend:${span.id}`,
|
|
1659
|
+
type: span.type,
|
|
1660
|
+
label: span.label,
|
|
1661
|
+
execution: "observed",
|
|
1662
|
+
evidenceLevel: "backend_observed",
|
|
1663
|
+
evidence: span.evidence ?? ["backend_observed: server-side method span reported by dev bridge"],
|
|
1664
|
+
...(span.nodeId ? { nodeId: span.nodeId } : {}),
|
|
1665
|
+
...(span.filePath ? { filePath: span.filePath } : {}),
|
|
1666
|
+
...(span.lineNumber !== undefined ? { lineNumber: span.lineNumber } : {}),
|
|
1667
|
+
startOffsetMs: span.startOffsetMs,
|
|
1668
|
+
durationMs: span.durationMs,
|
|
1669
|
+
...(span.status !== undefined ? { status: span.status } : {})
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
function diagramLayers(record) {
|
|
1673
|
+
if (!record.backendSpans?.length) {
|
|
1674
|
+
return orderedLayers(record).filter((layer) => layer.type !== "page");
|
|
1675
|
+
}
|
|
1676
|
+
return summaryRows(record).filter((layer) => !isUnprovenLayer(layer));
|
|
1677
|
+
}
|
|
1678
|
+
function requestOverheadLayer(record, api, result) {
|
|
1679
|
+
if (!api || !record.backendSpans?.length) {
|
|
1680
|
+
return api;
|
|
1681
|
+
}
|
|
1682
|
+
const backendSpans = record.backendSpans.filter((span) => span.type !== "api");
|
|
1683
|
+
const total = Math.max(1, record.durationMs ?? record.duration ?? api.durationMs ?? 1);
|
|
1684
|
+
const backendCoveredDuration = coveredDurationMs(backendSpans);
|
|
1685
|
+
const resultDuration = Math.max(0, result?.durationMs ?? 0);
|
|
1686
|
+
const durationMs = Math.max(0, total - backendCoveredDuration - resultDuration);
|
|
1687
|
+
return {
|
|
1688
|
+
...api,
|
|
1689
|
+
label: `${record.method} ${record.path}`,
|
|
1690
|
+
startOffsetMs: 0,
|
|
1691
|
+
durationMs
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
function summarizeBackendObservedSpans(spans) {
|
|
1695
|
+
const groupedSpans = new Map();
|
|
1696
|
+
const databaseSpans = [];
|
|
1697
|
+
const untrackedControllerSpans = [];
|
|
1698
|
+
const selfDurations = backendSelfDurationBySpanId(spans);
|
|
1699
|
+
for (const span of [...spans].sort(compareBackendSpans)) {
|
|
1700
|
+
if (span.type === "api") {
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
if (span.type === "controller") {
|
|
1704
|
+
untrackedControllerSpans.push(span);
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1707
|
+
if (span.type === "database") {
|
|
1708
|
+
databaseSpans.push(span);
|
|
1709
|
+
continue;
|
|
1710
|
+
}
|
|
1711
|
+
const key = `${span.type}:${span.label}`;
|
|
1712
|
+
groupedSpans.set(key, [...(groupedSpans.get(key) ?? []), span]);
|
|
1713
|
+
}
|
|
1714
|
+
const rows = [...groupedSpans.values()].map((group) => groupedBackendSpansToLayer(group, selfDurations));
|
|
1715
|
+
if (databaseSpans.length > 0) {
|
|
1716
|
+
rows.push(databaseSpansToSummaryLayer(databaseSpans, selfDurations));
|
|
1717
|
+
}
|
|
1718
|
+
if (untrackedControllerSpans.length > 0) {
|
|
1719
|
+
rows.push(untrackedControllerSpansToLayer(untrackedControllerSpans, selfDurations));
|
|
1720
|
+
}
|
|
1721
|
+
return rows.sort(compareFlowLayersByTiming);
|
|
1722
|
+
}
|
|
1723
|
+
function groupedBackendSpansToLayer(spans, selfDurations) {
|
|
1724
|
+
if (spans.length === 1) {
|
|
1725
|
+
const span = spans[0];
|
|
1726
|
+
return backendSpanToLayer({
|
|
1727
|
+
...span,
|
|
1728
|
+
durationMs: Math.max(0, Math.round(selfDurations.get(span.id) ?? span.durationMs))
|
|
1729
|
+
});
|
|
1730
|
+
}
|
|
1731
|
+
const first = spans[0];
|
|
1732
|
+
const startOffsetMs = Math.min(...spans.map((span) => span.startOffsetMs));
|
|
1733
|
+
const evidence = spans.flatMap((span) => span.evidence ?? []);
|
|
1734
|
+
const selfDurationMs = spans.reduce((sum, span) => sum + Math.max(0, selfDurations.get(span.id) ?? span.durationMs), 0);
|
|
1735
|
+
return {
|
|
1736
|
+
id: `backend:group:${first.type}:${first.label}`,
|
|
1737
|
+
type: first.type,
|
|
1738
|
+
label: `${first.label} ×${spans.length}`,
|
|
1739
|
+
execution: "observed",
|
|
1740
|
+
evidenceLevel: "backend_observed",
|
|
1741
|
+
evidence: [
|
|
1742
|
+
`backend_observed: ${spans.length} runtime ${first.type} span${spans.length === 1 ? "" : "s"} grouped by label`,
|
|
1743
|
+
...evidence.slice(0, 2)
|
|
1744
|
+
],
|
|
1745
|
+
startOffsetMs,
|
|
1746
|
+
durationMs: Math.max(0, Math.round(selfDurationMs))
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
function databaseSpansToSummaryLayer(spans, selfDurations) {
|
|
1750
|
+
const startOffsetMs = Math.min(...spans.map((span) => span.startOffsetMs));
|
|
1751
|
+
const firstEvidence = spans.flatMap((span) => span.evidence ?? [])[0];
|
|
1752
|
+
const selfDurationMs = spans.reduce((sum, span) => sum + Math.max(0, selfDurations.get(span.id) ?? span.durationMs), 0);
|
|
1753
|
+
return {
|
|
1754
|
+
id: "backend:database-summary",
|
|
1755
|
+
type: "database",
|
|
1756
|
+
label: `${spans.length} JDBC statement${spans.length === 1 ? "" : "s"}`,
|
|
1757
|
+
execution: "observed",
|
|
1758
|
+
evidenceLevel: "backend_observed",
|
|
1759
|
+
evidence: [
|
|
1760
|
+
`backend_observed: ${spans.length} JDBC execute span${spans.length === 1 ? "" : "s"} reported by dev bridge`,
|
|
1761
|
+
...(firstEvidence ? [firstEvidence] : [])
|
|
1762
|
+
],
|
|
1763
|
+
startOffsetMs,
|
|
1764
|
+
durationMs: Math.max(0, Math.round(selfDurationMs))
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function untrackedControllerSpansToLayer(spans, selfDurations) {
|
|
1768
|
+
const startOffsetMs = Math.min(...spans.map((span) => span.startOffsetMs));
|
|
1769
|
+
const selfDurationMs = spans.reduce((sum, span) => sum + Math.max(0, selfDurations.get(span.id) ?? 0), 0);
|
|
1770
|
+
return {
|
|
1771
|
+
id: "backend:untracked-app",
|
|
1772
|
+
type: "unknown",
|
|
1773
|
+
label: "Untracked application time",
|
|
1774
|
+
execution: "observed",
|
|
1775
|
+
evidenceLevel: "backend_observed",
|
|
1776
|
+
evidence: [
|
|
1777
|
+
"backend_observed: controller runtime remained after measured child spans were removed"
|
|
1778
|
+
],
|
|
1779
|
+
startOffsetMs,
|
|
1780
|
+
durationMs: Math.max(0, Math.round(selfDurationMs))
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
function backendSelfDurationBySpanId(spans) {
|
|
1784
|
+
const childrenByParent = new Map();
|
|
1785
|
+
for (const span of spans) {
|
|
1786
|
+
if (!span.parentId) {
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
childrenByParent.set(span.parentId, [...(childrenByParent.get(span.parentId) ?? []), span]);
|
|
1790
|
+
}
|
|
1791
|
+
return new Map(spans.map((span) => {
|
|
1792
|
+
const childDuration = coveredDurationMs(childrenByParent.get(span.id) ?? []);
|
|
1793
|
+
return [span.id, Math.max(0, span.durationMs - childDuration)];
|
|
1794
|
+
}));
|
|
1795
|
+
}
|
|
1796
|
+
function coveredDurationMs(spans) {
|
|
1797
|
+
const intervals = spans
|
|
1798
|
+
.filter((span) => span.durationMs > 0)
|
|
1799
|
+
.map((span) => ({
|
|
1800
|
+
start: span.startOffsetMs,
|
|
1801
|
+
end: span.startOffsetMs + span.durationMs
|
|
1802
|
+
}))
|
|
1803
|
+
.sort((left, right) => left.start - right.start);
|
|
1804
|
+
let covered = 0;
|
|
1805
|
+
let currentStart;
|
|
1806
|
+
let currentEnd;
|
|
1807
|
+
for (const interval of intervals) {
|
|
1808
|
+
if (currentStart === undefined || currentEnd === undefined) {
|
|
1809
|
+
currentStart = interval.start;
|
|
1810
|
+
currentEnd = interval.end;
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
if (interval.start <= currentEnd) {
|
|
1814
|
+
currentEnd = Math.max(currentEnd, interval.end);
|
|
1815
|
+
continue;
|
|
1816
|
+
}
|
|
1817
|
+
covered += currentEnd - currentStart;
|
|
1818
|
+
currentStart = interval.start;
|
|
1819
|
+
currentEnd = interval.end;
|
|
1820
|
+
}
|
|
1821
|
+
if (currentStart !== undefined && currentEnd !== undefined) {
|
|
1822
|
+
covered += currentEnd - currentStart;
|
|
1823
|
+
}
|
|
1824
|
+
return Math.max(0, covered);
|
|
1825
|
+
}
|
|
1826
|
+
function findFirstLayer(layers, types) {
|
|
1827
|
+
return layers.find((layer) => types.includes(layer.type));
|
|
1828
|
+
}
|
|
1829
|
+
function compareBackendSpans(a, b) {
|
|
1830
|
+
const timingDelta = a.startOffsetMs - b.startOffsetMs;
|
|
1831
|
+
if (timingDelta !== 0)
|
|
1832
|
+
return timingDelta;
|
|
1833
|
+
if (a.id === b.parentId)
|
|
1834
|
+
return -1;
|
|
1835
|
+
if (a.parentId === b.id)
|
|
1836
|
+
return 1;
|
|
1837
|
+
return layerOrderIndex(a.type) - layerOrderIndex(b.type) || a.label.localeCompare(b.label);
|
|
1838
|
+
}
|
|
1839
|
+
function compareFlowLayersByTiming(a, b) {
|
|
1840
|
+
const timingDelta = (a.startOffsetMs ?? Number.MAX_SAFE_INTEGER) - (b.startOffsetMs ?? Number.MAX_SAFE_INTEGER);
|
|
1841
|
+
if (timingDelta !== 0)
|
|
1842
|
+
return timingDelta;
|
|
1843
|
+
return layerOrderIndex(a.type) - layerOrderIndex(b.type) || a.label.localeCompare(b.label);
|
|
1844
|
+
}
|
|
1845
|
+
function layerOrderIndex(type) {
|
|
1846
|
+
const index = layerOrder.indexOf(type);
|
|
1847
|
+
return index === -1 ? layerOrder.length : index;
|
|
1848
|
+
}
|
|
1849
|
+
function isUnprovenLayer(layer) {
|
|
1850
|
+
return layer.execution === "not_proven" || layer.evidenceLevel === "not_proven";
|
|
1851
|
+
}
|
|
1852
|
+
function isUntrackedAppLayer(layer) {
|
|
1853
|
+
return layer.id === "backend:untracked-app";
|
|
1854
|
+
}
|
|
1855
|
+
function isSourceMatchedLayer(layer) {
|
|
1856
|
+
return layer.execution === "scanned" || layer.evidenceLevel === "source_derived";
|
|
1857
|
+
}
|
|
1858
|
+
function isDecisionLayer(layer) {
|
|
1859
|
+
return layer.type === "auth" || layer.type === "decision";
|
|
1860
|
+
}
|
|
1861
|
+
function summaryStatusLabel(layer, locale) {
|
|
1862
|
+
if (isUnprovenLayer(layer))
|
|
1863
|
+
return t(locale, "notProven");
|
|
1864
|
+
if (layer.execution === "blocked")
|
|
1865
|
+
return t(locale, "blocked");
|
|
1866
|
+
if (layer.evidenceLevel === "browser_observed" || layer.evidenceLevel === "backend_observed") {
|
|
1867
|
+
return t(locale, "observed");
|
|
1868
|
+
}
|
|
1869
|
+
if (layer.evidenceLevel === "inferred")
|
|
1870
|
+
return t(locale, "inferred");
|
|
1871
|
+
if (isSourceMatchedLayer(layer))
|
|
1872
|
+
return t(locale, "sourceMatched");
|
|
1873
|
+
return t(locale, "matched");
|
|
1874
|
+
}
|
|
1875
|
+
function summaryStatusTone(layer) {
|
|
1876
|
+
if (isUnprovenLayer(layer) || isSourceMatchedLayer(layer))
|
|
1877
|
+
return "scanned";
|
|
1878
|
+
if (layer.execution === "blocked")
|
|
1879
|
+
return "blocked";
|
|
1880
|
+
if (layer.evidenceLevel === "inferred")
|
|
1881
|
+
return "inferred";
|
|
1882
|
+
return "matched";
|
|
1883
|
+
}
|
|
1884
|
+
function visualLayerType(layer) {
|
|
1885
|
+
if (isUntrackedAppLayer(layer))
|
|
1886
|
+
return "untracked";
|
|
1887
|
+
if (layer.type === "decision")
|
|
1888
|
+
return "auth";
|
|
1889
|
+
if (layer.type === "action" ||
|
|
1890
|
+
layer.type === "api" ||
|
|
1891
|
+
layer.type === "controller" ||
|
|
1892
|
+
layer.type === "auth" ||
|
|
1893
|
+
layer.type === "result" ||
|
|
1894
|
+
layer.type === "service" ||
|
|
1895
|
+
layer.type === "repository" ||
|
|
1896
|
+
layer.type === "database") {
|
|
1897
|
+
return layer.type;
|
|
1898
|
+
}
|
|
1899
|
+
return "controller";
|
|
1900
|
+
}
|
|
1901
|
+
function layerLabel(layer, locale = "en") {
|
|
1902
|
+
if (isUntrackedAppLayer(layer))
|
|
1903
|
+
return t(locale, "untrackedApp");
|
|
1904
|
+
if (layer.type === "api")
|
|
1905
|
+
return "API";
|
|
1906
|
+
if (layer.type === "auth")
|
|
1907
|
+
return locale === "ko" ? "인증 / 세션" : "Auth / Session";
|
|
1908
|
+
if (layer.type === "action")
|
|
1909
|
+
return locale === "ko" ? "사용자 액션" : "Action";
|
|
1910
|
+
if (layer.type === "controller")
|
|
1911
|
+
return locale === "ko" ? "컨트롤러" : "Controller";
|
|
1912
|
+
if (layer.type === "decision")
|
|
1913
|
+
return locale === "ko" ? "결정" : "Decision";
|
|
1914
|
+
if (layer.type === "service")
|
|
1915
|
+
return t(locale, "service");
|
|
1916
|
+
if (layer.type === "repository")
|
|
1917
|
+
return t(locale, "repository");
|
|
1918
|
+
if (layer.type === "database")
|
|
1919
|
+
return t(locale, "database");
|
|
1920
|
+
if (layer.type === "result")
|
|
1921
|
+
return t(locale, "result");
|
|
1922
|
+
return capitalize(layer.type);
|
|
1923
|
+
}
|
|
1924
|
+
function estimatedLayerDuration(layer, total) {
|
|
1925
|
+
if (layer.durationMs !== undefined) {
|
|
1926
|
+
return layer.durationMs;
|
|
1927
|
+
}
|
|
1928
|
+
if (isUnprovenLayer(layer)) {
|
|
1929
|
+
return Math.max(1, Math.round(total * 0.06));
|
|
1930
|
+
}
|
|
1931
|
+
if (layer.type === "result") {
|
|
1932
|
+
return 1;
|
|
1933
|
+
}
|
|
1934
|
+
return Math.max(1, Math.round(total * 0.18));
|
|
1935
|
+
}
|
|
1936
|
+
function layerOffset(layer, total) {
|
|
1937
|
+
if (layer.startOffsetMs !== undefined) {
|
|
1938
|
+
return Math.min(96, Math.max(0, (layer.startOffsetMs / Math.max(total, 1)) * 100));
|
|
1939
|
+
}
|
|
1940
|
+
if (layer.type === "action" || layer.type === "api")
|
|
1941
|
+
return 0;
|
|
1942
|
+
if (layer.type === "controller")
|
|
1943
|
+
return 2;
|
|
1944
|
+
if (isDecisionLayer(layer))
|
|
1945
|
+
return 10;
|
|
1946
|
+
if (layer.type === "result")
|
|
1947
|
+
return 76;
|
|
1948
|
+
return 9;
|
|
1949
|
+
}
|
|
1950
|
+
function timelineTicks(total) {
|
|
1951
|
+
const steps = 4;
|
|
1952
|
+
return Array.from({ length: steps + 1 }, (_, index) => (total / steps) * index);
|
|
1953
|
+
}
|
|
1954
|
+
function timingBreakdownSegments(rows, total) {
|
|
1955
|
+
const types = [
|
|
1956
|
+
"api",
|
|
1957
|
+
"controller",
|
|
1958
|
+
"service",
|
|
1959
|
+
"repository",
|
|
1960
|
+
"database",
|
|
1961
|
+
"untracked",
|
|
1962
|
+
"result"
|
|
1963
|
+
];
|
|
1964
|
+
const labels = {
|
|
1965
|
+
api: "API",
|
|
1966
|
+
controller: "Controller",
|
|
1967
|
+
service: "Service",
|
|
1968
|
+
repository: "Repository",
|
|
1969
|
+
database: "Database",
|
|
1970
|
+
untracked: "Untracked",
|
|
1971
|
+
result: "Result"
|
|
1972
|
+
};
|
|
1973
|
+
const durations = new Map(types.map((type) => [type, 0]));
|
|
1974
|
+
for (const row of rows) {
|
|
1975
|
+
if (isUnprovenLayer(row)) {
|
|
1976
|
+
continue;
|
|
1977
|
+
}
|
|
1978
|
+
const type = isUntrackedAppLayer(row) ? "untracked" : row.type;
|
|
1979
|
+
if (!types.includes(type)) {
|
|
1980
|
+
continue;
|
|
1981
|
+
}
|
|
1982
|
+
durations.set(type, (durations.get(type) ?? 0) + Math.max(0, row.durationMs ?? 0));
|
|
1983
|
+
}
|
|
1984
|
+
const rawNonApiDuration = types
|
|
1985
|
+
.filter((type) => type !== "api")
|
|
1986
|
+
.reduce((sum, type) => sum + (durations.get(type) ?? 0), 0);
|
|
1987
|
+
const nonApiScale = rawNonApiDuration > total ? total / rawNonApiDuration : 1;
|
|
1988
|
+
if (nonApiScale < 1) {
|
|
1989
|
+
for (const type of types) {
|
|
1990
|
+
if (type !== "api") {
|
|
1991
|
+
durations.set(type, (durations.get(type) ?? 0) * nonApiScale);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
const nonApiDuration = types
|
|
1996
|
+
.filter((type) => type !== "api")
|
|
1997
|
+
.reduce((sum, type) => sum + (durations.get(type) ?? 0), 0);
|
|
1998
|
+
durations.set("api", Math.max(0, total - nonApiDuration));
|
|
1999
|
+
const measuredTotal = Math.max(total, 1);
|
|
2000
|
+
return types.map((type) => {
|
|
2001
|
+
const durationMs = durations.get(type) ?? 0;
|
|
2002
|
+
return {
|
|
2003
|
+
durationMs,
|
|
2004
|
+
label: labels[type],
|
|
2005
|
+
type,
|
|
2006
|
+
width: durationMs <= 0 ? 0 : (durationMs / measuredTotal) * 100
|
|
2007
|
+
};
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
function timelineX(percent) {
|
|
2011
|
+
const clamped = Math.min(100, Math.max(0, percent));
|
|
2012
|
+
return TIMELINE_START_INSET_PERCENT + (clamped / 100) * (100 - TIMELINE_START_INSET_PERCENT);
|
|
2013
|
+
}
|
|
2014
|
+
function timelineWidth(left, width) {
|
|
2015
|
+
const visualLeft = timelineX(left);
|
|
2016
|
+
const visualRight = timelineX(Math.min(100, left + width));
|
|
2017
|
+
return Math.max(0, visualRight - visualLeft);
|
|
2018
|
+
}
|
|
2019
|
+
function hasBlockedOutcome(record) {
|
|
2020
|
+
return record.status !== undefined && [401, 403, 409].includes(record.status);
|
|
2021
|
+
}
|
|
2022
|
+
function OutcomeIcon({ record, size }) {
|
|
2023
|
+
if (hasBlockedOutcome(record)) {
|
|
2024
|
+
return _jsx(Lock, { size: size });
|
|
2025
|
+
}
|
|
2026
|
+
if (record.status !== undefined && record.status >= 500) {
|
|
2027
|
+
return _jsx(Flag, { size: size });
|
|
2028
|
+
}
|
|
2029
|
+
return _jsx(Check, { size: size });
|
|
2030
|
+
}
|
|
2031
|
+
function outcomeLabel(record, locale) {
|
|
2032
|
+
if (record.status === undefined)
|
|
2033
|
+
return t(locale, "pending");
|
|
2034
|
+
if (record.status >= 200 && record.status < 300)
|
|
2035
|
+
return t(locale, "ok");
|
|
2036
|
+
if (hasBlockedOutcome(record))
|
|
2037
|
+
return t(locale, "blocked");
|
|
2038
|
+
if (record.status >= 500)
|
|
2039
|
+
return t(locale, "serverError");
|
|
2040
|
+
return locale === "ko" ? "관찰됨" : "Observed";
|
|
2041
|
+
}
|
|
2042
|
+
function outcomeStatusText(record, locale) {
|
|
2043
|
+
if (record.status === undefined)
|
|
2044
|
+
return t(locale, "pending");
|
|
2045
|
+
if (record.status >= 200 && record.status < 300)
|
|
2046
|
+
return t(locale, "ok");
|
|
2047
|
+
if (record.status === 401)
|
|
2048
|
+
return t(locale, "unauthorized");
|
|
2049
|
+
if (record.status === 403)
|
|
2050
|
+
return t(locale, "forbidden");
|
|
2051
|
+
if (record.status === 409)
|
|
2052
|
+
return t(locale, "conflict");
|
|
2053
|
+
if (record.status >= 500)
|
|
2054
|
+
return t(locale, "serverError");
|
|
2055
|
+
return locale === "ko" ? "관찰됨" : "Observed";
|
|
2056
|
+
}
|
|
2057
|
+
function outcomeTone(record) {
|
|
2058
|
+
if (record.status !== undefined && record.status >= 200 && record.status < 300)
|
|
2059
|
+
return "success";
|
|
2060
|
+
if (hasBlockedOutcome(record))
|
|
2061
|
+
return "blocked";
|
|
2062
|
+
return "error";
|
|
2063
|
+
}
|
|
2064
|
+
function outcomeDescription(record, locale) {
|
|
2065
|
+
if (record.status !== undefined && record.status >= 200 && record.status < 300) {
|
|
2066
|
+
if (record.matchState !== "matched") {
|
|
2067
|
+
if (record.backendSpans?.length) {
|
|
2068
|
+
return locale === "ko"
|
|
2069
|
+
? "요청이 완료되었고 개발 런타임 span이 관찰되었습니다. 가져온 Flow JSON과는 아직 매칭되지 않았습니다."
|
|
2070
|
+
: "Request completed with development runtime spans observed. It is not matched to imported Flow JSON yet.";
|
|
2071
|
+
}
|
|
2072
|
+
return locale === "ko"
|
|
2073
|
+
? "요청이 완료되었지만 가져온 Flow JSON과는 아직 매칭되지 않았습니다."
|
|
2074
|
+
: "Request completed, but it is not matched to imported Flow JSON yet.";
|
|
2075
|
+
}
|
|
2076
|
+
return locale === "ko"
|
|
2077
|
+
? "요청이 완료되었고 가져온 백엔드 흐름과 매칭되었습니다."
|
|
2078
|
+
: "Request completed and matched imported backend flow.";
|
|
2079
|
+
}
|
|
2080
|
+
if (record.status === 409) {
|
|
2081
|
+
return locale === "ko"
|
|
2082
|
+
? "요청이 결정 분기에 도달했고 차단 결과를 반환했습니다."
|
|
2083
|
+
: "Request reached a decision branch and returned a blocked result.";
|
|
2084
|
+
}
|
|
2085
|
+
if (hasBlockedOutcome(record)) {
|
|
2086
|
+
return locale === "ko"
|
|
2087
|
+
? "하위 비즈니스 로직의 실행이 확인되기 전에 요청이 차단되었습니다."
|
|
2088
|
+
: "Request blocked before downstream business logic was proven executed.";
|
|
2089
|
+
}
|
|
2090
|
+
return locale === "ko"
|
|
2091
|
+
? "브라우저에서 관찰된 요청과 소스 기반 백엔드 매칭입니다. 서버 런타임 트레이스는 아닙니다."
|
|
2092
|
+
: "Browser-observed request with source-derived backend matching. This is not a server runtime trace.";
|
|
2093
|
+
}
|
|
2094
|
+
function requestContextDescription(record, locale) {
|
|
2095
|
+
const hasRuntimeSpans = Boolean(record.backendSpans?.length);
|
|
2096
|
+
if (record.matchState === "matched" && hasRuntimeSpans) {
|
|
2097
|
+
return t(locale, "mappedEvidenceWithRuntime");
|
|
2098
|
+
}
|
|
2099
|
+
if (record.matchState === "matched") {
|
|
2100
|
+
return t(locale, "mappedEvidence");
|
|
2101
|
+
}
|
|
2102
|
+
if (hasRuntimeSpans) {
|
|
2103
|
+
return locale === "ko"
|
|
2104
|
+
? "브라우저에서 관찰된 요청에 개발 런타임 span이 연결되었습니다. 가져온 Flow JSON 매칭은 아직 없습니다."
|
|
2105
|
+
: "Browser-observed request with development runtime spans. No imported Flow JSON match yet.";
|
|
2106
|
+
}
|
|
2107
|
+
return locale === "ko"
|
|
2108
|
+
? "브라우저에서 관찰된 요청입니다. 가져온 Flow JSON 매칭은 아직 없습니다."
|
|
2109
|
+
: "Browser-observed request. No imported Flow JSON match yet.";
|
|
2110
|
+
}
|
|
2111
|
+
function flowNote(record, locale) {
|
|
2112
|
+
if (record.backendSpans?.length) {
|
|
2113
|
+
return locale === "ko"
|
|
2114
|
+
? "관찰됨으로 표시된 백엔드 행은 이 요청에 대한 개발 전용 백엔드 브리지에서 왔습니다. 소스 행은 가져온 근거를 기반으로 합니다."
|
|
2115
|
+
: "Backend rows marked observed came from a development-only backend bridge for this request. Source rows still come from imported evidence.";
|
|
2116
|
+
}
|
|
2117
|
+
const hasUnprovenDownstream = record.layers.some((layer) => (layer.type === "service" || layer.type === "repository" || layer.type === "database") &&
|
|
2118
|
+
isUnprovenLayer(layer));
|
|
2119
|
+
if (hasUnprovenDownstream) {
|
|
2120
|
+
return locale === "ko"
|
|
2121
|
+
? "흐리게 표시된 레이어는 소스 근거로만 알려진 경로이며 실제 실행은 확인되지 않았습니다."
|
|
2122
|
+
: "Muted layers are known by source evidence only and were not proven executed.";
|
|
2123
|
+
}
|
|
2124
|
+
return locale === "ko"
|
|
2125
|
+
? "브라우저 행은 페이지에서 관찰된 내용입니다. 소스 행은 가져온 백엔드 근거이며 운영 런타임 트레이스가 아닙니다."
|
|
2126
|
+
: "Browser rows are observed in the page. Source rows come from imported backend evidence and are not a production runtime trace.";
|
|
2127
|
+
}
|
|
2128
|
+
function confidenceDescription(record, locale) {
|
|
2129
|
+
if (record.confidence === "high") {
|
|
2130
|
+
return locale === "ko"
|
|
2131
|
+
? "브라우저 요청, 엔드포인트, 컨트롤러, 소스 근거가 모두 매칭되었습니다."
|
|
2132
|
+
: "Browser request, endpoint, controller, and source evidence all matched.";
|
|
2133
|
+
}
|
|
2134
|
+
if (record.confidence === "medium") {
|
|
2135
|
+
return locale === "ko"
|
|
2136
|
+
? "브라우저 요청은 소스 근거와 매칭되었지만 일부 백엔드 세부 정보는 추론입니다."
|
|
2137
|
+
: "The browser request matched source evidence, but some backend details are inferred.";
|
|
2138
|
+
}
|
|
2139
|
+
return locale === "ko"
|
|
2140
|
+
? "Anlyx가 브라우저 요청을 관찰했지만 백엔드 소스 근거가 충분하지 않습니다."
|
|
2141
|
+
: "Anlyx observed the browser request, but backend source evidence is incomplete.";
|
|
2142
|
+
}
|
|
2143
|
+
function evidenceCoverage(record) {
|
|
2144
|
+
return record.layers.reduce((coverage, layer) => {
|
|
2145
|
+
if (layer.evidenceLevel === "browser_observed") {
|
|
2146
|
+
coverage.browser += 1;
|
|
2147
|
+
}
|
|
2148
|
+
else if (layer.evidenceLevel === "backend_observed") {
|
|
2149
|
+
coverage.backend += 1;
|
|
2150
|
+
}
|
|
2151
|
+
else if (isUnprovenLayer(layer)) {
|
|
2152
|
+
coverage.notProven += 1;
|
|
2153
|
+
}
|
|
2154
|
+
else {
|
|
2155
|
+
coverage.source += 1;
|
|
2156
|
+
}
|
|
2157
|
+
return coverage;
|
|
2158
|
+
}, { browser: 0, backend: record.backendSpans?.length ?? 0, source: 0, notProven: 0 });
|
|
2159
|
+
}
|
|
2160
|
+
function layerSubtitle(layer, locale, runtimeSource = "browser") {
|
|
2161
|
+
if (layer.type === "action")
|
|
2162
|
+
return locale === "ko" ? "사용자 클릭 캡처됨" : "user click captured";
|
|
2163
|
+
if (layer.type === "api") {
|
|
2164
|
+
return runtimeSource === "server" ? t(locale, "serverRequestSpan") : t(locale, "browserSpan");
|
|
2165
|
+
}
|
|
2166
|
+
if (isUnprovenLayer(layer))
|
|
2167
|
+
return t(locale, "knownBySourceOnly");
|
|
2168
|
+
if (layer.evidenceLevel === "browser_observed")
|
|
2169
|
+
return t(locale, "browserObserved");
|
|
2170
|
+
if (layer.evidenceLevel === "backend_observed")
|
|
2171
|
+
return t(locale, "devRuntimeSpan");
|
|
2172
|
+
if (isSourceMatchedLayer(layer) || layer.evidenceLevel === "inferred") {
|
|
2173
|
+
return t(locale, "sourceDerivedEstimate");
|
|
2174
|
+
}
|
|
2175
|
+
return executionLabel(layer.execution, locale);
|
|
2176
|
+
}
|
|
2177
|
+
function backendRuntimeGroupCount(layer) {
|
|
2178
|
+
if (layer.evidenceLevel !== "backend_observed") {
|
|
2179
|
+
return undefined;
|
|
2180
|
+
}
|
|
2181
|
+
const jdbcMatch = /^(\d+)\s+JDBC statement/.exec(layer.label);
|
|
2182
|
+
if (jdbcMatch?.[1]) {
|
|
2183
|
+
return Number(jdbcMatch[1]);
|
|
2184
|
+
}
|
|
2185
|
+
const groupedMatch = /×(\d+)$/.exec(layer.label);
|
|
2186
|
+
if (groupedMatch?.[1]) {
|
|
2187
|
+
return Number(groupedMatch[1]);
|
|
2188
|
+
}
|
|
2189
|
+
return undefined;
|
|
2190
|
+
}
|
|
2191
|
+
function durationValueLabel(_layer, duration) {
|
|
2192
|
+
return `${Math.round(duration)} ms`;
|
|
2193
|
+
}
|
|
2194
|
+
function durationCaption(layer, duration, total, locale, runtimeSource = "browser", runtimeGroupCount) {
|
|
2195
|
+
if (isUntrackedAppLayer(layer))
|
|
2196
|
+
return t(locale, "untrackedRuntime");
|
|
2197
|
+
if (layer.type === "result")
|
|
2198
|
+
return t(locale, "responseMarker");
|
|
2199
|
+
if (layer.type === "action" || layer.type === "api") {
|
|
2200
|
+
return runtimeSource === "server" ? t(locale, "serverRequestSpan") : t(locale, "browserSpan");
|
|
2201
|
+
}
|
|
2202
|
+
if (layer.evidenceLevel === "backend_observed") {
|
|
2203
|
+
if (runtimeGroupCount) {
|
|
2204
|
+
return `${runtimeGroupCount} ${t(locale, "callCount")}`;
|
|
2205
|
+
}
|
|
2206
|
+
return t(locale, "measuredRuntime");
|
|
2207
|
+
}
|
|
2208
|
+
return `${t(locale, "estimate")} · ${Math.round((duration / Math.max(total, 1)) * 100)}%`;
|
|
2209
|
+
}
|
|
2210
|
+
function tabLabel(tab, locale) {
|
|
2211
|
+
if (locale === "ko") {
|
|
2212
|
+
if (tab === "summary")
|
|
2213
|
+
return "요약";
|
|
2214
|
+
if (tab === "timing")
|
|
2215
|
+
return "타이밍";
|
|
2216
|
+
return "다이어그램";
|
|
2217
|
+
}
|
|
2218
|
+
return capitalize(tab);
|
|
2219
|
+
}
|
|
2220
|
+
function executionLabel(execution, locale) {
|
|
2221
|
+
if (execution === "blocked")
|
|
2222
|
+
return t(locale, "blocked");
|
|
2223
|
+
if (execution === "inferred")
|
|
2224
|
+
return t(locale, "inferred");
|
|
2225
|
+
if (execution === "not_proven")
|
|
2226
|
+
return t(locale, "notProven");
|
|
2227
|
+
if (execution === "observed")
|
|
2228
|
+
return t(locale, "observed");
|
|
2229
|
+
if (execution === "scanned")
|
|
2230
|
+
return locale === "ko" ? "소스 매칭" : "source matched";
|
|
2231
|
+
if (execution === "executed")
|
|
2232
|
+
return locale === "ko" ? "실행됨" : "executed";
|
|
2233
|
+
return execution.replace("_", " ");
|
|
2234
|
+
}
|
|
2235
|
+
function confidenceLabel(confidence, locale) {
|
|
2236
|
+
if (locale === "ko") {
|
|
2237
|
+
if (confidence === "high")
|
|
2238
|
+
return "높음";
|
|
2239
|
+
if (confidence === "medium")
|
|
2240
|
+
return "보통";
|
|
2241
|
+
return "낮음";
|
|
2242
|
+
}
|
|
2243
|
+
return capitalize(confidence);
|
|
2244
|
+
}
|
|
2245
|
+
function translateEvidence(value, locale) {
|
|
2246
|
+
if (locale === "en")
|
|
2247
|
+
return value;
|
|
2248
|
+
const normalized = value.toLowerCase();
|
|
2249
|
+
if (normalized.includes("click") || normalized.includes("browser-observed request")) {
|
|
2250
|
+
return "브라우저에서 사용자 액션을 관찰했습니다";
|
|
2251
|
+
}
|
|
2252
|
+
if (normalized.includes("fetch") || normalized.includes("xmlhttprequest")) {
|
|
2253
|
+
return "브라우저 fetch/XHR 요청을 관찰했습니다";
|
|
2254
|
+
}
|
|
2255
|
+
if (normalized.includes("endpoint")) {
|
|
2256
|
+
return "가져온 엔드포인트와 매칭되었습니다";
|
|
2257
|
+
}
|
|
2258
|
+
if (normalized.includes("controller")) {
|
|
2259
|
+
return "컨트롤러 근거가 매칭되었습니다";
|
|
2260
|
+
}
|
|
2261
|
+
if (normalized.includes("backend_observed") || normalized.includes("runtime")) {
|
|
2262
|
+
return "개발용 백엔드 브리지에서 런타임 span을 받았습니다";
|
|
2263
|
+
}
|
|
2264
|
+
if (normalized.includes("source-derived") || normalized.includes("source")) {
|
|
2265
|
+
return "소스 근거 기반 백엔드 매칭입니다";
|
|
2266
|
+
}
|
|
2267
|
+
if (normalized.includes("not a runtime") || normalized.includes("not_proven")) {
|
|
2268
|
+
return "운영 런타임 트레이스가 아니며 실행이 확인되지 않은 구간이 있습니다";
|
|
2269
|
+
}
|
|
2270
|
+
return value;
|
|
2271
|
+
}
|
|
2272
|
+
function compactId(value) {
|
|
2273
|
+
return value.replace(/^browser-request:/, "").slice(0, 8);
|
|
2274
|
+
}
|
|
2275
|
+
function shortPath(value) {
|
|
2276
|
+
return value.length > 30 ? `${value.slice(0, 27)}...` : value;
|
|
2277
|
+
}
|
|
2278
|
+
function formatDateTime(value) {
|
|
2279
|
+
if (!value) {
|
|
2280
|
+
return "Waiting for request";
|
|
2281
|
+
}
|
|
2282
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
2283
|
+
hour: "2-digit",
|
|
2284
|
+
minute: "2-digit",
|
|
2285
|
+
second: "2-digit"
|
|
2286
|
+
}).format(new Date(value));
|
|
2287
|
+
}
|
|
2288
|
+
function formatDuration(value) {
|
|
2289
|
+
return `${Math.round(value ?? 0)} ms`;
|
|
2290
|
+
}
|
|
2291
|
+
function capitalize(value) {
|
|
2292
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
2293
|
+
}
|