@anlyx/ui 0.1.3 → 0.1.6-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,838 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Background, BaseEdge, Controls, Handle, Position, ReactFlow, getSmoothStepPath } from "@xyflow/react";
3
+ import { MoreHorizontal, Pause, Play, RotateCcw, Search, SlidersHorizontal, Workflow, X } from "lucide-react";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import ELKConstructor from "elkjs/lib/elk.bundled.js";
6
+ const Elk = ELKConstructor;
7
+ const elk = new Elk();
8
+ const KIND_LABEL = {
9
+ frontend_file: "Frontend",
10
+ frontend_request: "Request",
11
+ api_endpoint: "API",
12
+ controller: "Controller",
13
+ service: "Service",
14
+ repository: "Repository",
15
+ db_table: "DB table",
16
+ collapsed: "Group"
17
+ };
18
+ const KIND_COLOR = {
19
+ frontend_file: "#0f766e",
20
+ frontend_request: "#0891b2",
21
+ api_endpoint: "#2563eb",
22
+ controller: "#7c3aed",
23
+ service: "#d97706",
24
+ repository: "#16a34a",
25
+ db_table: "#ea580c",
26
+ collapsed: "#64748b"
27
+ };
28
+ const DOMAIN_Y = {
29
+ Benefits: 120,
30
+ Admin: 245,
31
+ Auth: 380,
32
+ Shared: 520,
33
+ Users: 660,
34
+ Membership: 800,
35
+ Recommendations: 940,
36
+ Analytics: 1080,
37
+ Data: 650
38
+ };
39
+ const LAYER_X = {
40
+ frontend_file: 220,
41
+ frontend_request: 360,
42
+ api_endpoint: 520,
43
+ controller: 690,
44
+ service: 875,
45
+ repository: 1105,
46
+ db_table: 1325,
47
+ collapsed: 210
48
+ };
49
+ const VISIBLE_STATS = {
50
+ visible: "primary paths",
51
+ hover: "hover adds shared deps",
52
+ confidence: "high"
53
+ };
54
+ const UNDERLYING_STATS = {
55
+ frontendRequests: 56,
56
+ apiEndpoints: 39,
57
+ controllers: 13,
58
+ services: 19,
59
+ repositories: 13,
60
+ dbTables: 16
61
+ };
62
+ const NODE_SIZE_BY_KIND = {
63
+ frontend_file: { width: 42, height: 34 },
64
+ frontend_request: { width: 198, height: 42 },
65
+ api_endpoint: { width: 198, height: 42 },
66
+ controller: { width: 212, height: 48 },
67
+ service: { width: 210, height: 52 },
68
+ repository: { width: 218, height: 52 },
69
+ db_table: { width: 148, height: 42 },
70
+ collapsed: { width: 204, height: 54 }
71
+ };
72
+ const HUB_NODE_SIZE = { width: 224, height: 60 };
73
+ const TOOLTIP_WIDTH = 260;
74
+ const TOOLTIP_HEIGHT = 190;
75
+ const TOOLTIP_GAP = 14;
76
+ const COLUMN_GUIDES = [
77
+ { label: "Request group", left: "8.5%" },
78
+ { label: "Request", left: "23%" },
79
+ { label: "API endpoint", left: "35.6%" },
80
+ { label: "Controller", left: "47.6%" },
81
+ { label: "Service", left: "59.9%" },
82
+ { label: "Repository", left: "74.6%" },
83
+ { label: "DB table", left: "88.8%" }
84
+ ];
85
+ function getTreeNodeSize(kind, hub) {
86
+ if (hub) {
87
+ return HUB_NODE_SIZE;
88
+ }
89
+ return NODE_SIZE_BY_KIND[kind];
90
+ }
91
+ function n(id, kind, label, domain, layerOffset = 0, rowOffset = 0, extra = {}) {
92
+ const hub = extra.hub === true;
93
+ const collapsed = kind === "collapsed";
94
+ const nodeData = {
95
+ label,
96
+ kind,
97
+ domain,
98
+ depth: layerDepth(kind),
99
+ confidence: "high",
100
+ quietLabel: !hub && !collapsed && kind === "frontend_file",
101
+ ...extra
102
+ };
103
+ const size = getTreeNodeSize(kind, hub);
104
+ return {
105
+ id,
106
+ type: "treeNode",
107
+ position: {
108
+ x: (kind === "collapsed" ? LAYER_X.frontend_file : LAYER_X[kind]) + layerOffset,
109
+ y: DOMAIN_Y[domain] + rowOffset
110
+ },
111
+ data: nodeData,
112
+ style: {
113
+ width: size.width,
114
+ height: size.height
115
+ }
116
+ };
117
+ }
118
+ function e(source, target, visualRole = "primary_path") {
119
+ return {
120
+ id: `${source}->${target}`,
121
+ source,
122
+ target,
123
+ type: "scanTreeEdge",
124
+ sourceHandle: "out",
125
+ targetHandle: "in",
126
+ data: { visualRole },
127
+ className: `scan-tree-edge scan-tree-edge--${visualRole.replace("_", "-")}`
128
+ };
129
+ }
130
+ function layerDepth(kind) {
131
+ switch (kind) {
132
+ case "frontend_file":
133
+ return 1;
134
+ case "frontend_request":
135
+ return 2;
136
+ case "api_endpoint":
137
+ return 3;
138
+ case "controller":
139
+ return 4;
140
+ case "service":
141
+ return 5;
142
+ case "repository":
143
+ return 6;
144
+ case "db_table":
145
+ return 7;
146
+ default:
147
+ return 1;
148
+ }
149
+ }
150
+ function buildTreeMock() {
151
+ const nodes = [
152
+ n("benefits-group", "collapsed", "Benefits requests +14", "Benefits", 0, -74, {
153
+ collapsedCount: 14,
154
+ file: "src/features/benefits/**"
155
+ }),
156
+ n("benefits-page", "frontend_file", "BenefitsPage.tsx", "Benefits", 0, -34, {
157
+ file: "src/features/benefits/BenefitsPage.tsx"
158
+ }),
159
+ n("benefit-list", "frontend_file", "BenefitList.tsx", "Benefits", 0, 4, {
160
+ file: "src/features/benefits/BenefitList.tsx"
161
+ }),
162
+ n("use-benefits", "frontend_file", "useBenefits.ts", "Benefits", 0, 42, {
163
+ file: "src/features/benefits/useBenefits.ts"
164
+ }),
165
+ n("req-benefits-get", "frontend_request", "axios.get('/api/benefits')", "Benefits", 0, -18),
166
+ n("req-benefits-post", "frontend_request", "axios.post('/api/benefits')", "Benefits", 0, 32),
167
+ n("api-benefits-get", "api_endpoint", "GET /api/benefits", "Benefits", 0, -16),
168
+ n("api-benefits-post", "api_endpoint", "POST /api/benefits", "Benefits", 0, 36),
169
+ n("benefit-controller", "controller", "BenefitController", "Benefits", 0, 8, {
170
+ file: "src/controllers/benefit.controller.ts"
171
+ }),
172
+ n("source-controller", "controller", "SourceController", "Benefits", -8, -70, {
173
+ file: "src/controllers/source.controller.ts"
174
+ }),
175
+ n("benefit-service", "service", "BenefitService", "Benefits", 0, 0, {
176
+ file: "src/services/benefit.service.ts",
177
+ hub: true
178
+ }),
179
+ n("source-service", "service", "SourceService", "Benefits", 25, -84, {
180
+ file: "src/services/source.service.ts"
181
+ }),
182
+ n("benefit-repo", "repository", "BenefitRepository", "Benefits", 0, 5, {
183
+ file: "src/repositories/benefit.repository.ts",
184
+ hub: true
185
+ }),
186
+ n("source-repo", "repository", "SourceRepository", "Benefits", 10, -70, {
187
+ file: "src/repositories/source.repository.ts"
188
+ }),
189
+ n("benefits-table", "db_table", "benefits", "Benefits", 0, -20),
190
+ n("benefit-sources-table", "db_table", "benefit_sources", "Benefits", 0, 22),
191
+ n("benefit-tags-table", "db_table", "benefit_tags", "Benefits", 0, 64),
192
+ n("admin-group", "collapsed", "Admin endpoints +9", "Admin", 0, -42, {
193
+ collapsedCount: 9,
194
+ file: "src/admin/**"
195
+ }),
196
+ n("admin-dashboard", "frontend_file", "AdminDashboard.tsx", "Admin", 0, -4, {
197
+ file: "src/admin/AdminDashboard.tsx"
198
+ }),
199
+ n("user-management", "frontend_file", "UserManagement.tsx", "Admin", 0, 36, {
200
+ file: "src/admin/UserManagement.tsx"
201
+ }),
202
+ n("req-admin-users", "frontend_request", "axios.get('/api/admin/users')", "Admin", 0, -12),
203
+ n("req-admin-stats", "frontend_request", "axios.get('/api/admin/stats')", "Admin", 0, 38),
204
+ n("api-admin-users", "api_endpoint", "GET /api/admin/users", "Admin", 0, -12),
205
+ n("api-admin-stats", "api_endpoint", "GET /api/admin/stats", "Admin", 0, 38),
206
+ n("admin-controller", "controller", "AdminController", "Admin", 40, 16, {
207
+ file: "src/controllers/admin.controller.ts",
208
+ hub: true
209
+ }),
210
+ n("auth-group", "collapsed", "Auth requests +8", "Auth", 0, -58, {
211
+ collapsedCount: 8,
212
+ file: "src/auth/**"
213
+ }),
214
+ n("login-form", "frontend_file", "LoginForm.tsx", "Auth", 0, -20, {
215
+ file: "src/auth/LoginForm.tsx"
216
+ }),
217
+ n("auth-context", "frontend_file", "AuthContext.tsx", "Auth", 0, 22, {
218
+ file: "src/auth/AuthContext.tsx"
219
+ }),
220
+ n("req-login", "frontend_request", "axios.post('/api/login')", "Auth", 0, -28),
221
+ n("req-me", "frontend_request", "axios.get('/api/me')", "Auth", 0, 22),
222
+ n("api-login", "api_endpoint", "POST /api/login", "Auth", 0, -28),
223
+ n("api-me", "api_endpoint", "GET /api/me", "Auth", 0, 22),
224
+ n("auth-controller", "controller", "AuthController", "Auth", 0, -4, {
225
+ file: "src/controllers/auth.controller.ts",
226
+ hub: true
227
+ }),
228
+ n("auth-service", "service", "AuthService", "Auth", -18, -10, {
229
+ file: "src/services/auth.service.ts",
230
+ hub: true
231
+ }),
232
+ n("token-service", "service", "TokenService", "Auth", 22, 56, {
233
+ file: "src/services/token.service.ts"
234
+ }),
235
+ n("shared-service", "service", "SharedService", "Shared", -18, 0, {
236
+ file: "src/services/shared.service.ts",
237
+ hub: true
238
+ }),
239
+ n("audit-service", "service", "AuditService", "Shared", 18, 72, {
240
+ file: "src/services/audit.service.ts"
241
+ }),
242
+ n("notification-service", "service", "NotificationService", "Shared", 25, -70, {
243
+ file: "src/services/notification.service.ts"
244
+ }),
245
+ n("users-group", "collapsed", "User flows +11", "Users", 0, -62, {
246
+ collapsedCount: 11,
247
+ file: "src/features/users/**"
248
+ }),
249
+ n("profile-page", "frontend_file", "ProfilePage.tsx", "Users", 0, -24, {
250
+ file: "src/features/users/ProfilePage.tsx"
251
+ }),
252
+ n("user-settings", "frontend_file", "UserSettings.tsx", "Users", 0, 16, {
253
+ file: "src/features/users/UserSettings.tsx"
254
+ }),
255
+ n("req-users", "frontend_request", "axios.get('/api/users')", "Users", 0, -28),
256
+ n("req-user-patch", "frontend_request", "axios.patch('/api/users/:id')", "Users", 0, 24),
257
+ n("api-users", "api_endpoint", "GET /api/users", "Users", 0, -28),
258
+ n("api-user-patch", "api_endpoint", "PATCH /api/users/:id", "Users", 0, 24),
259
+ n("user-controller", "controller", "UserController", "Users", -8, 0, {
260
+ file: "src/controllers/user.controller.ts",
261
+ hub: true
262
+ }),
263
+ n("user-service", "service", "UserService", "Users", -10, -18, {
264
+ file: "src/services/user.service.ts",
265
+ hub: true
266
+ }),
267
+ n("user-repo", "repository", "UserRepository", "Users", -12, -26, {
268
+ file: "src/repositories/user.repository.ts",
269
+ hub: true
270
+ }),
271
+ n("role-repo", "repository", "RoleRepository", "Users", 8, 38, {
272
+ file: "src/repositories/role.repository.ts",
273
+ hub: true
274
+ }),
275
+ n("users-table", "db_table", "users", "Users", 0, -42),
276
+ n("roles-table", "db_table", "roles", "Users", 0, 4),
277
+ n("permissions-table", "db_table", "permissions", "Users", 0, 50),
278
+ n("membership-page", "frontend_file", "MembershipPage.tsx", "Membership", 0, -38, {
279
+ file: "src/features/membership/MembershipPage.tsx"
280
+ }),
281
+ n("req-memberships", "frontend_request", "axios.get('/api/memberships')", "Membership", 0, -20),
282
+ n("api-memberships", "api_endpoint", "GET /api/memberships", "Membership", 0, -20),
283
+ n("membership-controller", "controller", "MembershipController", "Membership", 0, -8, {
284
+ displayLabel: "MembershipCtrl",
285
+ file: "src/controllers/membership.controller.ts",
286
+ hub: true
287
+ }),
288
+ n("membership-service", "service", "MembershipService", "Membership", 0, -2, {
289
+ file: "src/services/membership.service.ts",
290
+ hub: true
291
+ }),
292
+ n("membership-repo", "repository", "MembershipRepository", "Membership", 0, -6, {
293
+ file: "src/repositories/membership.repository.ts"
294
+ }),
295
+ n("membership-table", "db_table", "memberships", "Membership", 0, -18),
296
+ n("membership-plan-table", "db_table", "membership_plans", "Membership", 0, 26),
297
+ n("recommendation-group", "collapsed", "Recommendation calls +7", "Recommendations", 0, -50, {
298
+ collapsedCount: 7,
299
+ file: "src/features/recommendations/**"
300
+ }),
301
+ n("recommendation-page", "frontend_file", "RecommendationPage.tsx", "Recommendations", 0, -12, {
302
+ file: "src/features/recommendations/RecommendationPage.tsx"
303
+ }),
304
+ n("req-recommendations", "frontend_request", "axios.get('/api/recommendations')", "Recommendations", 0, -6),
305
+ n("api-recommendations", "api_endpoint", "GET /api/recommendations", "Recommendations", 0, -6),
306
+ n("recommendation-controller", "controller", "RecommendationController", "Recommendations", 0, 0, {
307
+ displayLabel: "RecommendationCtrl",
308
+ file: "src/controllers/recommendation.controller.ts"
309
+ }),
310
+ n("recommendation-service", "service", "RecommendationService", "Recommendations", 0, 0, {
311
+ displayLabel: "RecService",
312
+ file: "src/services/recommendation.service.ts"
313
+ }),
314
+ n("recommendation-repo", "repository", "RecommendationRepository", "Recommendations", 0, -10, {
315
+ displayLabel: "RecommendationRepo",
316
+ file: "src/repositories/recommendation.repository.ts"
317
+ }),
318
+ n("interaction-repo", "repository", "InteractionRepository", "Recommendations", 0, 36, {
319
+ file: "src/repositories/interaction.repository.ts"
320
+ }),
321
+ n("recommendations-table", "db_table", "recommendations", "Recommendations", 0, -20),
322
+ n("user-interactions-table", "db_table", "user_interactions", "Recommendations", 0, 26),
323
+ n("analytics-group", "collapsed", "Analytics jobs +6", "Analytics", 0, -34, {
324
+ collapsedCount: 6,
325
+ file: "src/analytics/**"
326
+ }),
327
+ n("req-page-views", "frontend_request", "trackPageView('/')", "Analytics", -185, 10),
328
+ n("api-page-views", "api_endpoint", "POST /api/analytics/page-view", "Analytics", 0, 10),
329
+ n("analytics-controller", "controller", "AnalyticsController", "Analytics", 0, 10, {
330
+ file: "src/controllers/analytics.controller.ts"
331
+ }),
332
+ n("pageview-service", "service", "PageViewAnalyticsService", "Analytics", 0, 10, {
333
+ displayLabel: "PageViewSvc",
334
+ file: "src/services/page-view-analytics.service.ts"
335
+ }),
336
+ n("pageview-repo", "repository", "PageViewRepository", "Analytics", 0, 10, {
337
+ file: "src/repositories/page-view.repository.ts"
338
+ }),
339
+ n("pageviews-table", "db_table", "page_views", "Analytics", 0, 10),
340
+ n("db-group", "collapsed", "DB tables +12", "Data", 0, 154, {
341
+ collapsedCount: 12,
342
+ file: "database/schema.sql"
343
+ }),
344
+ n("audit-log-table", "db_table", "audit_logs", "Data", 0, 108),
345
+ n("notifications-table", "db_table", "notifications", "Data", 0, 154)
346
+ ];
347
+ const edges = [
348
+ e("benefits-group", "req-benefits-get", "aggregate"),
349
+ e("benefits-page", "req-benefits-get", "low_confidence"),
350
+ e("benefit-list", "req-benefits-get", "low_confidence"),
351
+ e("use-benefits", "req-benefits-get", "low_confidence"),
352
+ e("use-benefits", "req-benefits-post", "low_confidence"),
353
+ e("req-benefits-get", "api-benefits-get"),
354
+ e("req-benefits-post", "api-benefits-post"),
355
+ e("api-benefits-get", "benefit-controller"),
356
+ e("api-benefits-post", "benefit-controller"),
357
+ e("benefit-controller", "benefit-service"),
358
+ e("source-controller", "source-service", "shared_dependency"),
359
+ e("benefit-service", "benefit-repo"),
360
+ e("benefit-service", "source-repo", "shared_dependency"),
361
+ e("source-service", "source-repo"),
362
+ e("benefit-repo", "benefits-table"),
363
+ e("source-repo", "benefit-sources-table"),
364
+ e("source-repo", "benefit-tags-table"),
365
+ e("admin-group", "req-admin-users", "aggregate"),
366
+ e("admin-dashboard", "req-admin-stats", "low_confidence"),
367
+ e("user-management", "req-admin-users", "low_confidence"),
368
+ e("req-admin-users", "api-admin-users"),
369
+ e("req-admin-stats", "api-admin-stats"),
370
+ e("api-admin-users", "admin-controller"),
371
+ e("api-admin-stats", "admin-controller"),
372
+ e("admin-controller", "user-service", "shared_dependency"),
373
+ e("admin-controller", "benefit-service", "shared_dependency"),
374
+ e("admin-controller", "audit-service", "shared_dependency"),
375
+ e("auth-group", "req-login", "aggregate"),
376
+ e("login-form", "req-login", "low_confidence"),
377
+ e("auth-context", "req-me", "low_confidence"),
378
+ e("req-login", "api-login"),
379
+ e("req-me", "api-me"),
380
+ e("api-login", "auth-controller"),
381
+ e("api-me", "auth-controller"),
382
+ e("auth-controller", "auth-service"),
383
+ e("auth-service", "token-service"),
384
+ e("auth-service", "user-service", "shared_dependency"),
385
+ e("auth-service", "role-repo", "shared_dependency"),
386
+ e("token-service", "user-repo", "shared_dependency"),
387
+ e("shared-service", "audit-service", "shared_dependency"),
388
+ e("shared-service", "notification-service", "shared_dependency"),
389
+ e("benefit-service", "shared-service", "shared_dependency"),
390
+ e("auth-service", "shared-service", "shared_dependency"),
391
+ e("user-service", "shared-service", "shared_dependency"),
392
+ e("notification-service", "notifications-table"),
393
+ e("audit-service", "audit-log-table"),
394
+ e("users-group", "req-users", "aggregate"),
395
+ e("profile-page", "req-users", "low_confidence"),
396
+ e("user-settings", "req-user-patch", "low_confidence"),
397
+ e("req-users", "api-users"),
398
+ e("req-user-patch", "api-user-patch"),
399
+ e("api-users", "user-controller"),
400
+ e("api-user-patch", "user-controller"),
401
+ e("user-controller", "user-service"),
402
+ e("user-service", "user-repo"),
403
+ e("user-service", "role-repo"),
404
+ e("user-repo", "users-table"),
405
+ e("role-repo", "roles-table"),
406
+ e("role-repo", "permissions-table"),
407
+ e("membership-page", "req-memberships", "low_confidence"),
408
+ e("req-memberships", "api-memberships"),
409
+ e("api-memberships", "membership-controller"),
410
+ e("membership-controller", "membership-service"),
411
+ e("membership-service", "membership-repo"),
412
+ e("membership-service", "user-service", "shared_dependency"),
413
+ e("membership-repo", "membership-table"),
414
+ e("membership-repo", "membership-plan-table"),
415
+ e("recommendation-group", "req-recommendations", "aggregate"),
416
+ e("recommendation-page", "req-recommendations", "low_confidence"),
417
+ e("req-recommendations", "api-recommendations"),
418
+ e("api-recommendations", "recommendation-controller"),
419
+ e("recommendation-controller", "recommendation-service"),
420
+ e("recommendation-service", "recommendation-repo"),
421
+ e("recommendation-service", "interaction-repo"),
422
+ e("recommendation-service", "user-service", "shared_dependency"),
423
+ e("recommendation-repo", "recommendations-table"),
424
+ e("interaction-repo", "user-interactions-table"),
425
+ e("analytics-group", "req-page-views", "aggregate"),
426
+ e("req-page-views", "api-page-views"),
427
+ e("api-page-views", "analytics-controller"),
428
+ e("analytics-controller", "pageview-service"),
429
+ e("pageview-service", "pageview-repo"),
430
+ e("pageview-repo", "pageviews-table"),
431
+ e("pageview-service", "shared-service", "shared_dependency"),
432
+ e("benefit-repo", "db-group", "shared_dependency"),
433
+ e("user-repo", "db-group", "shared_dependency"),
434
+ e("role-repo", "db-group", "shared_dependency")
435
+ ];
436
+ return { nodes, edges };
437
+ }
438
+ function connectedIds(nodeId, edges, includeTwoHop = false) {
439
+ const ids = new Set();
440
+ if (!nodeId)
441
+ return ids;
442
+ ids.add(nodeId);
443
+ for (const edge of edges) {
444
+ if (edge.source === nodeId)
445
+ ids.add(edge.target);
446
+ if (edge.target === nodeId)
447
+ ids.add(edge.source);
448
+ }
449
+ if (!includeTwoHop)
450
+ return ids;
451
+ const firstHop = [...ids];
452
+ for (const id of firstHop) {
453
+ for (const edge of edges) {
454
+ if (edge.source === id)
455
+ ids.add(edge.target);
456
+ if (edge.target === id)
457
+ ids.add(edge.source);
458
+ }
459
+ }
460
+ return ids;
461
+ }
462
+ function flowPathFrom(nodeId, nodes, edges) {
463
+ if (!nodeId) {
464
+ return { nodeIds: new Set(), edgeIds: new Set() };
465
+ }
466
+ const nodeById = new Map(nodes.map((node) => [node.id, node]));
467
+ const primaryEdges = edges.filter((edge) => edge.data?.visualRole === "primary_path" || edge.data?.visualRole === "aggregate");
468
+ const nodeIds = new Set([nodeId]);
469
+ const edgeIds = new Set();
470
+ const addUpstream = (id) => {
471
+ for (const edge of primaryEdges) {
472
+ if (edge.target !== id || edgeIds.has(edge.id))
473
+ continue;
474
+ const sourceKind = nodeById.get(edge.source)?.data.kind;
475
+ if (sourceKind === "frontend_file" || sourceKind === "db_table")
476
+ continue;
477
+ edgeIds.add(edge.id);
478
+ nodeIds.add(edge.source);
479
+ addUpstream(edge.source);
480
+ }
481
+ };
482
+ const addDownstream = (id) => {
483
+ for (const edge of primaryEdges) {
484
+ if (edge.source !== id || edgeIds.has(edge.id))
485
+ continue;
486
+ if (edge.data?.visualRole !== "primary_path")
487
+ continue;
488
+ edgeIds.add(edge.id);
489
+ nodeIds.add(edge.target);
490
+ addDownstream(edge.target);
491
+ }
492
+ };
493
+ addUpstream(nodeId);
494
+ addDownstream(nodeId);
495
+ return { nodeIds, edgeIds };
496
+ }
497
+ function isDefaultVisibleEdge(edge) {
498
+ return edge.data?.visualRole === "primary_path" || edge.data?.visualRole === "aggregate";
499
+ }
500
+ function nodeIdsFromEdges(edges) {
501
+ const ids = new Set();
502
+ for (const edge of edges) {
503
+ ids.add(edge.source);
504
+ ids.add(edge.target);
505
+ }
506
+ return ids;
507
+ }
508
+ function defaultReachableEdges(nodes, edges) {
509
+ const visibleCandidates = edges.filter(isDefaultVisibleEdge);
510
+ const reachable = new Set(nodes
511
+ .filter((node) => node.data.kind === "collapsed" || node.data.kind === "frontend_request")
512
+ .map((node) => node.id));
513
+ const selected = [];
514
+ const selectedIds = new Set();
515
+ let changed = true;
516
+ while (changed) {
517
+ changed = false;
518
+ for (const edge of visibleCandidates) {
519
+ if (!reachable.has(edge.source) || selectedIds.has(edge.id)) {
520
+ continue;
521
+ }
522
+ selected.push(edge);
523
+ selectedIds.add(edge.id);
524
+ if (!reachable.has(edge.target)) {
525
+ reachable.add(edge.target);
526
+ changed = true;
527
+ }
528
+ }
529
+ }
530
+ return selected;
531
+ }
532
+ async function layoutTreeWithElk(graph) {
533
+ const elkGraph = {
534
+ id: "anlyx-scan-map",
535
+ layoutOptions: {
536
+ "elk.algorithm": "layered",
537
+ "elk.direction": "RIGHT",
538
+ "elk.edgeRouting": "SPLINES",
539
+ "elk.spacing.nodeNode": "34",
540
+ "elk.layered.spacing.nodeNodeBetweenLayers": "72",
541
+ "elk.layered.spacing.edgeNodeBetweenLayers": "34",
542
+ "elk.layered.crossingMinimization.strategy": "LAYER_SWEEP",
543
+ "elk.layered.nodePlacement.strategy": "BRANDES_KOEPF",
544
+ "elk.layered.considerModelOrder.strategy": "NODES_AND_EDGES"
545
+ },
546
+ children: graph.nodes.map((node) => {
547
+ const hub = node.data.hub === true;
548
+ const size = getTreeNodeSize(node.data.kind, hub);
549
+ return {
550
+ id: node.id,
551
+ width: size.width,
552
+ height: size.height
553
+ };
554
+ }),
555
+ edges: graph.edges.map((edge) => ({
556
+ id: edge.id,
557
+ sources: [edge.source],
558
+ targets: [edge.target]
559
+ }))
560
+ };
561
+ const layoutedGraph = await elk.layout(elkGraph);
562
+ const positions = new Map(layoutedGraph.children?.map((node) => [node.id, { x: node.x ?? 0, y: node.y ?? 0 }]) ?? []);
563
+ return {
564
+ nodes: graph.nodes.map((node) => ({
565
+ ...node,
566
+ position: {
567
+ x: positions.get(node.id)?.x ?? node.position.x,
568
+ y: node.position.y
569
+ }
570
+ })),
571
+ edges: graph.edges
572
+ };
573
+ }
574
+ function ScanTreeEdge({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, data }) {
575
+ const visualRole = data?.visualRole ?? "primary_path";
576
+ const [edgePath] = getSmoothStepPath({
577
+ sourceX,
578
+ sourceY,
579
+ sourcePosition,
580
+ targetX,
581
+ targetY,
582
+ targetPosition,
583
+ borderRadius: visualRole === "aggregate" ? 18 : 14,
584
+ offset: visualRole === "shared_dependency" ? 18 : 12
585
+ });
586
+ return (_jsx(BaseEdge, { id: id, path: edgePath, className: `scan-tree-edge__path scan-tree-edge--${visualRole.replace("_", "-")}`, ...(markerEnd ? { markerEnd } : {}) }));
587
+ }
588
+ function TreeNode({ data, selected }) {
589
+ const kind = data.kind;
590
+ const color = KIND_COLOR[kind];
591
+ const hub = data.hub === true;
592
+ const collapsed = kind === "collapsed";
593
+ const displayLabel = data.displayLabel ??
594
+ (collapsed && data.collapsedCount ? data.label.replace(/\s\+\d+$/, "") : data.label);
595
+ return (_jsxs("div", { className: `scan-tree-node scan-tree-node--${kind} ${hub ? "is-hub" : ""} ${collapsed ? "is-collapsed" : ""} ${data.quietLabel ? "is-quiet-label" : ""} ${selected ? "is-selected" : ""}`, style: { "--node-color": color }, children: [_jsx(Handle, { type: "target", position: Position.Left, id: "in", className: "scan-tree-handle" }), _jsx("span", { className: "scan-tree-node__dot" }), _jsxs("span", { className: "scan-tree-node__label", children: [_jsx("strong", { children: displayLabel }), data.subtitle ? _jsx("small", { children: data.subtitle }) : null, collapsed && data.collapsedCount ? _jsx("small", { children: "collapsed dependency group" }) : null] }), collapsed && data.collapsedCount ? (_jsxs("span", { className: "scan-tree-node__count", children: ["+", data.collapsedCount] })) : null, _jsx(Handle, { type: "source", position: Position.Right, id: "out", className: "scan-tree-handle" })] }));
596
+ }
597
+ const nodeTypes = { treeNode: TreeNode };
598
+ const edgeTypes = { scanTreeEdge: ScanTreeEdge };
599
+ export function ScanTreeMap({ data }) {
600
+ const graph = useMemo(buildTreeMock, []);
601
+ const [layoutedGraph, setLayoutedGraph] = useState(graph);
602
+ const [hoveredNodeId, setHoveredNodeId] = useState(null);
603
+ const [selectedNodeId, setSelectedNodeId] = useState(null);
604
+ const [isolatedNodeId, setIsolatedNodeId] = useState(null);
605
+ const [tooltip, setTooltip] = useState(null);
606
+ const [flowPlaying, setFlowPlaying] = useState(true);
607
+ const [flowSpeed, setFlowSpeed] = useState(1);
608
+ const hoverClearTimer = useRef(null);
609
+ const reactFlowRef = useRef(null);
610
+ const persistentFocusId = isolatedNodeId ?? selectedNodeId;
611
+ const highlightId = hoveredNodeId ?? persistentFocusId;
612
+ useEffect(() => {
613
+ let cancelled = false;
614
+ layoutTreeWithElk(graph)
615
+ .then((nextGraph) => {
616
+ if (!cancelled) {
617
+ setLayoutedGraph(nextGraph);
618
+ }
619
+ })
620
+ .catch(() => {
621
+ if (!cancelled) {
622
+ setLayoutedGraph(graph);
623
+ }
624
+ });
625
+ return () => {
626
+ cancelled = true;
627
+ };
628
+ }, [graph]);
629
+ useEffect(() => () => {
630
+ if (hoverClearTimer.current) {
631
+ window.clearTimeout(hoverClearTimer.current);
632
+ }
633
+ }, []);
634
+ const defaultEdges = useMemo(() => defaultReachableEdges(layoutedGraph.nodes, layoutedGraph.edges), [layoutedGraph.edges, layoutedGraph.nodes]);
635
+ const selectedFlowPath = useMemo(() => flowPathFrom(persistentFocusId, layoutedGraph.nodes, layoutedGraph.edges), [layoutedGraph.edges, layoutedGraph.nodes, persistentFocusId]);
636
+ const activeIds = hoveredNodeId
637
+ ? connectedIds(hoveredNodeId, layoutedGraph.edges, false)
638
+ : isolatedNodeId
639
+ ? selectedFlowPath.nodeIds
640
+ : persistentFocusId
641
+ ? selectedFlowPath.nodeIds
642
+ : connectedIds(selectedNodeId, layoutedGraph.edges, selectedNodeId !== null);
643
+ const visibleEdges = useMemo(() => {
644
+ if (hoveredNodeId) {
645
+ const mergedEdges = new Map(defaultEdges.map((edge) => [edge.id, edge]));
646
+ for (const edge of layoutedGraph.edges) {
647
+ const isDirectlyConnected = edge.source === hoveredNodeId || edge.target === hoveredNodeId;
648
+ const canRevealOnHover = edge.data?.visualRole === "shared_dependency";
649
+ if (isDirectlyConnected && canRevealOnHover) {
650
+ mergedEdges.set(edge.id, edge);
651
+ }
652
+ }
653
+ return [...mergedEdges.values()];
654
+ }
655
+ if (!persistentFocusId) {
656
+ return defaultEdges;
657
+ }
658
+ return layoutedGraph.edges.filter((edge) => {
659
+ if (!activeIds.has(edge.source) || !activeIds.has(edge.target)) {
660
+ return false;
661
+ }
662
+ if (isolatedNodeId) {
663
+ return edge.data?.visualRole === "primary_path" || edge.data?.visualRole === "aggregate";
664
+ }
665
+ return edge.data?.visualRole !== "low_confidence";
666
+ });
667
+ }, [
668
+ activeIds,
669
+ defaultEdges,
670
+ hoveredNodeId,
671
+ isolatedNodeId,
672
+ layoutedGraph.edges,
673
+ persistentFocusId
674
+ ]);
675
+ const visibleNodeIds = useMemo(() => {
676
+ const ids = nodeIdsFromEdges(visibleEdges);
677
+ if (highlightId) {
678
+ ids.add(highlightId);
679
+ }
680
+ return ids;
681
+ }, [highlightId, visibleEdges]);
682
+ const nodes = useMemo(() => layoutedGraph.nodes
683
+ .filter((node) => visibleNodeIds.has(node.id))
684
+ .map((node) => {
685
+ const isFocused = node.id === highlightId;
686
+ return {
687
+ ...node,
688
+ selected: node.id === selectedNodeId,
689
+ className: `${node.className ?? ""} is-active ${isFocused ? "is-focused" : ""}`.trim()
690
+ };
691
+ }), [highlightId, layoutedGraph.nodes, selectedNodeId, visibleNodeIds]);
692
+ const edges = useMemo(() => visibleEdges.map((edge) => {
693
+ const isActive = highlightId !== null &&
694
+ ((activeIds.has(edge.source) && activeIds.has(edge.target)) ||
695
+ edge.source === highlightId ||
696
+ edge.target === highlightId);
697
+ const isLiveFlow = persistentFocusId !== null && selectedFlowPath.edgeIds.has(edge.id);
698
+ const isQuietAggregate = highlightId === null && edge.data?.visualRole === "aggregate";
699
+ return {
700
+ ...edge,
701
+ animated: isLiveFlow && flowPlaying,
702
+ className: `${edge.className ?? ""} ${isActive ? "is-active" : ""} ${isLiveFlow ? "is-live-flow" : ""} ${!flowPlaying ? "is-paused" : ""} flow-speed-${String(flowSpeed).replace(".", "-")} ${isQuietAggregate ? "is-aggregate-idle" : ""}`.trim()
703
+ };
704
+ }), [
705
+ activeIds,
706
+ flowPlaying,
707
+ flowSpeed,
708
+ highlightId,
709
+ persistentFocusId,
710
+ selectedFlowPath.edgeIds,
711
+ visibleEdges
712
+ ]);
713
+ useEffect(() => {
714
+ window.requestAnimationFrame(() => {
715
+ reactFlowRef.current?.fitView({ padding: 0.055, duration: 120 });
716
+ });
717
+ }, [edges.length, layoutedGraph, nodes.length]);
718
+ useEffect(() => {
719
+ function handleKeyDown(event) {
720
+ if (event.key !== "Escape")
721
+ return;
722
+ setSelectedNodeId(null);
723
+ setIsolatedNodeId(null);
724
+ setHoveredNodeId(null);
725
+ setTooltip(null);
726
+ }
727
+ window.addEventListener("keydown", handleKeyDown);
728
+ return () => window.removeEventListener("keydown", handleKeyDown);
729
+ }, []);
730
+ const selectedNode = layoutedGraph.nodes.find((node) => node.id === (persistentFocusId ?? ""));
731
+ const sourceLabel = data.projectName || "Anlyx workspace";
732
+ function keepHoverAlive() {
733
+ if (hoverClearTimer.current) {
734
+ window.clearTimeout(hoverClearTimer.current);
735
+ hoverClearTimer.current = null;
736
+ }
737
+ }
738
+ function clearHoverSoon() {
739
+ if (hoverClearTimer.current) {
740
+ window.clearTimeout(hoverClearTimer.current);
741
+ }
742
+ hoverClearTimer.current = window.setTimeout(() => {
743
+ setHoveredNodeId(null);
744
+ setTooltip(null);
745
+ hoverClearTimer.current = null;
746
+ }, 220);
747
+ }
748
+ function handleNodeHover(event, node) {
749
+ keepHoverAlive();
750
+ setHoveredNodeId(node.id);
751
+ setTooltip(positionTooltipForNode(event.currentTarget, node));
752
+ }
753
+ function handleNodeMove(event, node) {
754
+ keepHoverAlive();
755
+ setHoveredNodeId(node.id);
756
+ setTooltip((current) => {
757
+ if (current?.node.id === node.id) {
758
+ return current;
759
+ }
760
+ return positionTooltipForNode(event.currentTarget, node);
761
+ });
762
+ }
763
+ function handleNodeLeave() {
764
+ clearHoverSoon();
765
+ }
766
+ function clearFlowFocus() {
767
+ setSelectedNodeId(null);
768
+ setIsolatedNodeId(null);
769
+ setHoveredNodeId(null);
770
+ setTooltip(null);
771
+ }
772
+ function replayFlow() {
773
+ setFlowPlaying(false);
774
+ window.requestAnimationFrame(() => setFlowPlaying(true));
775
+ }
776
+ return (_jsxs("div", { className: "scan-tree-workspace", "data-testid": "map-graph", children: [_jsxs("header", { className: "scan-tree-header", children: [_jsxs("div", { children: [_jsxs("div", { className: "scan-tree-breadcrumbs", children: [_jsx("span", { children: "Flows" }), _jsx("span", { children: "/" }), _jsx("strong", { children: "Map" })] }), _jsxs("div", { className: "scan-tree-title-row", children: [_jsx("h1", { children: "Map" }), _jsx("span", { "aria-hidden": "true" })] }), _jsx("p", { children: "Primary request paths stay visible. Hover a node to reveal shared dependencies." })] }), _jsxs("div", { className: "scan-tree-header__meta", children: [_jsx("strong", { children: sourceLabel }), _jsx("span", { children: formatScanTime(data.generatedAt) }), _jsxs("small", { children: ["curated tree mock \u00B7 ", UNDERLYING_STATS.frontendRequests, "+ frontend requests"] })] })] }), _jsxs("div", { className: "scan-tree-toolbar", children: [_jsxs("label", { className: "scan-tree-search", children: [_jsx(Search, { size: 15 }), _jsx("input", { placeholder: "Search nodes", "aria-label": "Search nodes" })] }), _jsxs("div", { className: "scan-tree-toolbar__right", children: [_jsxs("button", { type: "button", children: [_jsx(SlidersHorizontal, { size: 15 }), "Filters"] }), _jsxs("span", { children: [_jsx(Workflow, { size: 15 }), "Layout: Layered Tree"] }), _jsx("span", { children: "Clusters: On" }), _jsx("button", { type: "button", "aria-label": "More options", children: _jsx(MoreHorizontal, { size: 16 }) })] })] }), _jsxs("div", { className: "scan-tree-metrics", children: [_jsxs("span", { children: [nodes.length, " visible nodes"] }), _jsxs("span", { children: [edges.length, " primary edges"] }), _jsx("span", { children: VISIBLE_STATS.visible }), _jsx("span", { children: VISIBLE_STATS.hover }), _jsxs("span", { children: [VISIBLE_STATS.confidence, " confidence"] })] }), _jsxs("section", { className: "scan-tree-canvas-shell", children: [_jsx("div", { className: "scan-tree-column-guides", "aria-hidden": "true", children: COLUMN_GUIDES.map((column) => (_jsx("span", { style: { left: column.left }, "data-label": column.label }, column.label))) }), _jsx("div", { className: "scan-tree-hint", children: "Default: primary paths \u00B7 Click request/API to replay flow \u00B7 Hover: shared dependencies" }), _jsxs(ReactFlow, { nodes: nodes, edges: edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, fitView: true, fitViewOptions: { padding: 0.052 }, minZoom: 0.34, maxZoom: 1.6, nodesDraggable: false, nodesConnectable: false, edgesFocusable: false, elementsSelectable: true, panOnScroll: true, proOptions: { hideAttribution: true }, onInit: (instance) => {
777
+ reactFlowRef.current = instance;
778
+ }, onNodeMouseEnter: handleNodeHover, onNodeMouseMove: handleNodeMove, onNodeMouseLeave: handleNodeLeave, onNodeClick: (_, node) => {
779
+ setSelectedNodeId((current) => (current === node.id ? null : node.id));
780
+ setIsolatedNodeId(null);
781
+ setFlowPlaying(true);
782
+ }, onNodeDoubleClick: (_, node) => {
783
+ setIsolatedNodeId((current) => (current === node.id ? null : node.id));
784
+ setSelectedNodeId(node.id);
785
+ }, onPaneClick: () => {
786
+ setSelectedNodeId(null);
787
+ setIsolatedNodeId(null);
788
+ setHoveredNodeId(null);
789
+ setTooltip(null);
790
+ }, children: [_jsx(Background, { color: "#dde7f0", gap: 18, size: 1 }), _jsx(Controls, { showInteractive: false, className: "scan-tree-controls" })] }), _jsxs("details", { className: "scan-tree-legend", open: true, children: [_jsx("summary", { children: "Legend" }), _jsx("div", { children: [
791
+ "collapsed",
792
+ "frontend_file",
793
+ "frontend_request",
794
+ "api_endpoint",
795
+ "controller",
796
+ "service",
797
+ "repository",
798
+ "db_table"
799
+ ].map((kind) => (_jsxs("span", { children: [_jsx("i", { style: { background: KIND_COLOR[kind] } }), KIND_LABEL[kind]] }, kind))) })] }), selectedNode ? (_jsxs("div", { className: "scan-tree-live-flow-pill", children: [_jsx("button", { type: "button", "aria-label": flowPlaying ? "Pause live flow" : "Play live flow", onClick: () => setFlowPlaying((current) => !current), children: flowPlaying ? _jsx(Pause, { size: 13 }) : _jsx(Play, { size: 13 }) }), _jsxs("span", { children: ["Live flow:", " ", _jsx("strong", { children: selectedNode.data.displayLabel ?? selectedNode.data.label })] }), _jsx("button", { type: "button", "aria-label": "Replay live flow", onClick: replayFlow, children: _jsx(RotateCcw, { size: 13 }) }), _jsxs("select", { "aria-label": "Live flow speed", value: flowSpeed, onChange: (event) => setFlowSpeed(Number(event.target.value)), children: [_jsx("option", { value: 0.5, children: "0.5x" }), _jsx("option", { value: 1, children: "1.0x" }), _jsx("option", { value: 2, children: "2.0x" })] }), _jsx("button", { type: "button", "aria-label": "Clear flow focus", onClick: clearFlowFocus, children: _jsx(X, { size: 13 }) })] })) : null, tooltip ? (_jsx(ScanTreeTooltip, { tooltip: tooltip, edgeCount: countEdges(tooltip.node.id, layoutedGraph.edges), onMouseEnter: keepHoverAlive, onMouseLeave: clearHoverSoon })) : null] })] }));
800
+ }
801
+ function ScanTreeTooltip({ edgeCount, onMouseEnter, onMouseLeave, tooltip }) {
802
+ const { node, x, y } = tooltip;
803
+ return (_jsxs("div", { className: "scan-tree-tooltip", style: { left: x, top: y }, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, children: [_jsx("strong", { children: node.data.label }), _jsxs("dl", { children: [_jsxs("div", { children: [_jsx("dt", { children: "Type" }), _jsx("dd", { children: KIND_LABEL[node.data.kind] })] }), _jsxs("div", { children: [_jsx("dt", { children: "Domain" }), _jsx("dd", { children: node.data.domain })] }), _jsxs("div", { children: [_jsx("dt", { children: "File" }), _jsx("dd", { children: node.data.file ?? "derived dependency node" })] }), _jsxs("div", { children: [_jsx("dt", { children: "Connected" }), _jsxs("dd", { children: [edgeCount, " nodes"] })] }), _jsxs("div", { children: [_jsx("dt", { children: "Depth" }), _jsx("dd", { children: node.data.depth })] }), _jsxs("div", { children: [_jsx("dt", { children: "Confidence" }), _jsx("dd", { children: node.data.confidence })] })] })] }));
804
+ }
805
+ function positionTooltipForNode(target, node) {
806
+ const element = target instanceof HTMLElement ? target : undefined;
807
+ const rect = element?.getBoundingClientRect();
808
+ if (!rect) {
809
+ return { x: TOOLTIP_GAP, y: TOOLTIP_GAP, node };
810
+ }
811
+ const viewportWidth = window.innerWidth;
812
+ const viewportHeight = window.innerHeight;
813
+ const hasRightSpace = rect.right + TOOLTIP_GAP + TOOLTIP_WIDTH <= viewportWidth - TOOLTIP_GAP;
814
+ const preferredX = hasRightSpace
815
+ ? rect.right + TOOLTIP_GAP
816
+ : rect.left - TOOLTIP_GAP - TOOLTIP_WIDTH;
817
+ const x = clamp(preferredX, TOOLTIP_GAP, viewportWidth - TOOLTIP_WIDTH - TOOLTIP_GAP);
818
+ const centeredY = rect.top + rect.height / 2 - TOOLTIP_HEIGHT / 2;
819
+ const y = clamp(centeredY, TOOLTIP_GAP, viewportHeight - TOOLTIP_HEIGHT - TOOLTIP_GAP);
820
+ return { x, y, node };
821
+ }
822
+ function clamp(value, min, max) {
823
+ return Math.min(Math.max(value, min), Math.max(min, max));
824
+ }
825
+ function countEdges(nodeId, edges) {
826
+ return edges.filter((edge) => edge.source === nodeId || edge.target === nodeId).length;
827
+ }
828
+ function formatScanTime(value) {
829
+ const date = new Date(value);
830
+ if (Number.isNaN(date.getTime()))
831
+ return value;
832
+ return new Intl.DateTimeFormat("ko-KR", {
833
+ month: "numeric",
834
+ day: "numeric",
835
+ hour: "2-digit",
836
+ minute: "2-digit"
837
+ }).format(date);
838
+ }