@wtdlee/repomap 0.8.0 → 0.9.1

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.
@@ -1,5 +1,5 @@
1
- import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';var o=class{constructor(t){this.rootPath=t;}result=null;async generate(t={}){if(!this.rootPath)throw new Error("Root path required for analysis");let{title:e="Rails Application Map"}=t;this.result=await k(this.rootPath);let s=this.generateHTML(e);return t.outputPath&&(d.writeFileSync(t.outputPath,s),console.log(`
2
- \u{1F4C4} Generated: ${t.outputPath}`)),s}generateFromResult(t,e="Rails Application Map"){return this.result=t,this.generateHTML(e)}generateHTML(t){if(!this.result)throw new Error("Analysis not run");let{routes:e,controllers:s,models:a,grpc:l,summary:i}=this.result;return `<!DOCTYPE html>
1
+ import {k}from'./chunk-H7VVRHQZ.js';import*as c from'fs';import*as d from'path';var i=class{constructor(t){this.rootPath=t;}result=null;async generate(t={}){if(!this.rootPath)throw new Error("Root path required for analysis");let{title:e="Rails Application Map"}=t;this.result=await k(this.rootPath);let s=this.generateHTML(e);return t.outputPath&&(c.writeFileSync(t.outputPath,s),console.log(`
2
+ \u{1F4C4} Generated: ${t.outputPath}`)),s}generateFromResult(t,e="Rails Application Map"){return this.result=t,this.generateHTML(e)}generateHTML(t){if(!this.result)throw new Error("Analysis not run");let{routes:e,controllers:s,models:a,grpc:l,summary:n}=this.result;return `<!DOCTYPE html>
3
3
  <html lang="en">
4
4
  <head>
5
5
  <meta charset="UTF-8">
@@ -23,25 +23,25 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
23
23
  <div class="stats-bar">
24
24
  <div class="stat active" data-view="routes">
25
25
  <div>
26
- <div class="stat-value">${i.totalRoutes.toLocaleString()}</div>
26
+ <div class="stat-value">${n.totalRoutes.toLocaleString()}</div>
27
27
  <div class="stat-label">Routes</div>
28
28
  </div>
29
29
  </div>
30
30
  <div class="stat" data-view="controllers">
31
31
  <div>
32
- <div class="stat-value">${i.totalControllers}</div>
32
+ <div class="stat-value">${n.totalControllers}</div>
33
33
  <div class="stat-label">Controllers</div>
34
34
  </div>
35
35
  </div>
36
36
  <div class="stat" data-view="models">
37
37
  <div>
38
- <div class="stat-value">${i.totalModels}</div>
38
+ <div class="stat-value">${n.totalModels}</div>
39
39
  <div class="stat-label">Models</div>
40
40
  </div>
41
41
  </div>
42
42
  <div class="stat" data-view="grpc">
43
43
  <div>
44
- <div class="stat-value">${i.totalGrpcServices}</div>
44
+ <div class="stat-value">${n.totalGrpcServices}</div>
45
45
  <div class="stat-label">gRPC</div>
46
46
  </div>
47
47
  </div>
@@ -62,7 +62,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
62
62
  </div>
63
63
 
64
64
  <div class="sidebar-section namespaces" id="namespaceFilter">
65
- <div class="sidebar-title">Namespaces (${i.namespaces.length})</div>
65
+ <div class="sidebar-title">Namespaces (${n.namespaces.length})</div>
66
66
  <div class="namespace-list">
67
67
  <div class="namespace-item active" data-namespace="all">
68
68
  <span>All</span>
@@ -103,6 +103,11 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
103
103
  let currentView = 'routes';
104
104
  let selectedNamespaces = new Set(['all']);
105
105
  let selectedMethods = new Set(['all']);
106
+ let selectedControllerFlags = new Set(['all']);
107
+ let selectedModelNamespaces = new Set(['all']);
108
+ let selectedModelFlags = new Set(['all']);
109
+ let selectedGrpcNamespaces = new Set(['all']);
110
+ let selectedGrpcFlags = new Set(['all']);
106
111
  let searchQuery = '';
107
112
  let routesDisplayCount = 200;
108
113
  let controllersDisplayCount = 50;
@@ -177,68 +182,83 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
177
182
  });
178
183
  });
179
184
 
180
- // Namespace filter (multi-select with Ctrl/Cmd)
181
- document.querySelectorAll('.namespace-item[data-namespace]').forEach(item => {
182
- item.addEventListener('click', (e) => {
183
- const ns = item.dataset.namespace;
184
- if (e.ctrlKey || e.metaKey) {
185
- // Multi-select
186
- if (ns === 'all') {
187
- selectedNamespaces = new Set(['all']);
188
- } else {
189
- selectedNamespaces.delete('all');
190
- if (selectedNamespaces.has(ns)) {
191
- selectedNamespaces.delete(ns);
192
- if (selectedNamespaces.size === 0) selectedNamespaces.add('all');
193
- } else {
194
- selectedNamespaces.add(ns);
195
- }
196
- }
197
- } else {
198
- // Single select
199
- selectedNamespaces = new Set([ns]);
200
- }
201
- updateFilterUI();
185
+ // Sidebar filter click handler (works even after sidebar re-render)
186
+ document.addEventListener('click', (e) => {
187
+ const target = e.target instanceof Element ? e.target : e.target?.parentElement;
188
+ if (!target) return;
189
+ const item = target.closest('.namespace-item');
190
+ if (!item) return;
191
+
192
+ const filterType = item.dataset.filterType;
193
+ const value = item.dataset.filterValue;
194
+ if (!filterType || value === undefined) return;
195
+
196
+ const multi = e.ctrlKey || e.metaKey;
197
+
198
+ function toggleMulti(setRef, v) {
199
+ if (v === 'all') return new Set(['all']);
200
+ const next = new Set(setRef);
201
+ next.delete('all');
202
+ if (next.has(v)) next.delete(v);
203
+ else next.add(v);
204
+ if (next.size === 0) next.add('all');
205
+ return next;
206
+ }
207
+
208
+ function toggleSingle(v) {
209
+ return new Set([v]);
210
+ }
211
+
212
+ if (filterType === 'routeNamespace') {
213
+ selectedNamespaces = multi ? toggleMulti(selectedNamespaces, value) : toggleSingle(value);
202
214
  routesDisplayCount = 200;
203
215
  saveStateToUrl();
204
- renderMainPanel();
205
- });
206
- });
207
-
208
- // Method filter (multi-select with Ctrl/Cmd)
209
- document.querySelectorAll('.namespace-item[data-method]').forEach(item => {
210
- item.addEventListener('click', (e) => {
211
- const method = item.dataset.method;
212
- if (e.ctrlKey || e.metaKey) {
213
- if (selectedMethods.has('all')) {
214
- selectedMethods = new Set([method]);
215
- } else if (selectedMethods.has(method)) {
216
- selectedMethods.delete(method);
217
- if (selectedMethods.size === 0) selectedMethods.add('all');
218
- } else {
219
- selectedMethods.add(method);
220
- }
221
- } else {
222
- if (selectedMethods.has(method) && selectedMethods.size === 1) {
223
- selectedMethods = new Set(['all']);
224
- } else {
225
- selectedMethods = new Set([method]);
226
- }
227
- }
228
- updateFilterUI();
216
+ } else if (filterType === 'routeMethod') {
217
+ // Methods behave similarly to namespaces
218
+ selectedMethods = multi ? toggleMulti(selectedMethods, value) : toggleSingle(value);
229
219
  routesDisplayCount = 200;
230
220
  saveStateToUrl();
231
- renderMainPanel();
232
- });
221
+ } else if (filterType === 'controllerFlag') {
222
+ selectedControllerFlags = multi
223
+ ? toggleMulti(selectedControllerFlags, value)
224
+ : toggleSingle(value);
225
+ controllersDisplayCount = 50;
226
+ } else if (filterType === 'modelNamespace') {
227
+ selectedModelNamespaces = multi
228
+ ? toggleMulti(selectedModelNamespaces, value)
229
+ : toggleSingle(value);
230
+ modelsDisplayCount = 50;
231
+ } else if (filterType === 'modelFlag') {
232
+ selectedModelFlags = multi ? toggleMulti(selectedModelFlags, value) : toggleSingle(value);
233
+ modelsDisplayCount = 50;
234
+ } else if (filterType === 'grpcNamespace') {
235
+ selectedGrpcNamespaces = multi ? toggleMulti(selectedGrpcNamespaces, value) : toggleSingle(value);
236
+ grpcDisplayCount = 50;
237
+ } else if (filterType === 'grpcFlag') {
238
+ selectedGrpcFlags = multi ? toggleMulti(selectedGrpcFlags, value) : toggleSingle(value);
239
+ grpcDisplayCount = 50;
240
+ }
241
+
242
+ updateFilterUI();
243
+ renderMainPanel();
233
244
  });
234
245
 
235
246
  function updateFilterUI() {
236
- document.querySelectorAll('.namespace-item[data-namespace]').forEach(item => {
237
- item.classList.toggle('active', selectedNamespaces.has(item.dataset.namespace));
238
- });
239
- document.querySelectorAll('.namespace-item[data-method]').forEach(item => {
240
- // Only highlight selected methods, not all when 'all' is selected
241
- item.classList.toggle('active', selectedMethods.has(item.dataset.method));
247
+ document.querySelectorAll('.namespace-item[data-filter-type][data-filter-value]').forEach(item => {
248
+ const t = item.dataset.filterType;
249
+ const v = item.dataset.filterValue;
250
+ if (!t || v === undefined) return;
251
+
252
+ let active = false;
253
+ if (t === 'routeNamespace') active = selectedNamespaces.has(v);
254
+ else if (t === 'routeMethod') active = selectedMethods.has(v);
255
+ else if (t === 'controllerFlag') active = selectedControllerFlags.has(v);
256
+ else if (t === 'modelNamespace') active = selectedModelNamespaces.has(v);
257
+ else if (t === 'modelFlag') active = selectedModelFlags.has(v);
258
+ else if (t === 'grpcNamespace') active = selectedGrpcNamespaces.has(v);
259
+ else if (t === 'grpcFlag') active = selectedGrpcFlags.has(v);
260
+
261
+ item.classList.toggle('active', active);
242
262
  });
243
263
  }
244
264
 
@@ -250,19 +270,10 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
250
270
 
251
271
  // Render Functions
252
272
  function renderMainPanel() {
253
- // Disable filters for models/diagram tabs
273
+ // Update sidebar filters per view (routes/controllers vs models vs grpc)
254
274
  const namespaceFilter = document.getElementById('namespaceFilter');
255
275
  const methodFilter = document.getElementById('methodFilter');
256
- const filtersDisabled = currentView === 'models' || currentView === 'diagram';
257
-
258
- if (namespaceFilter) {
259
- namespaceFilter.style.opacity = filtersDisabled ? '0.4' : '1';
260
- namespaceFilter.style.pointerEvents = filtersDisabled ? 'none' : 'auto';
261
- }
262
- if (methodFilter) {
263
- methodFilter.style.opacity = filtersDisabled ? '0.4' : '1';
264
- methodFilter.style.pointerEvents = filtersDisabled ? 'none' : 'auto';
265
- }
276
+ renderSidebarFilters(namespaceFilter, methodFilter);
266
277
 
267
278
  switch (currentView) {
268
279
  case 'routes':
@@ -285,6 +296,289 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
285
296
  attachEventListeners();
286
297
  }
287
298
 
299
+ function renderSidebarFilters(namespaceFilter, methodFilter) {
300
+ if (!namespaceFilter || !methodFilter) return;
301
+
302
+ function sectionHtml(title, listClass, inner) {
303
+ return (
304
+ '<div class="sidebar-title">' +
305
+ title +
306
+ '</div>' +
307
+ '<div class="' +
308
+ listClass +
309
+ '">' +
310
+ inner +
311
+ '</div>'
312
+ );
313
+ }
314
+
315
+ if (currentView === 'routes') {
316
+ namespaceFilter.style.opacity = '1';
317
+ namespaceFilter.style.pointerEvents = 'auto';
318
+ methodFilter.style.opacity = '1';
319
+ methodFilter.style.pointerEvents = 'auto';
320
+
321
+ namespaceFilter.innerHTML = sectionHtml(
322
+ 'Namespaces',
323
+ 'namespace-list',
324
+ renderRouteNamespaceFilters()
325
+ );
326
+ methodFilter.innerHTML = sectionHtml(
327
+ 'HTTP Methods',
328
+ 'namespace-list methods-list',
329
+ renderRouteMethodFilters()
330
+ );
331
+ return;
332
+ }
333
+
334
+ if (currentView === 'controllers') {
335
+ namespaceFilter.style.opacity = '1';
336
+ namespaceFilter.style.pointerEvents = 'auto';
337
+ methodFilter.style.opacity = '1';
338
+ methodFilter.style.pointerEvents = 'auto';
339
+
340
+ namespaceFilter.innerHTML = sectionHtml(
341
+ 'Controller Namespaces',
342
+ 'namespace-list',
343
+ renderControllerNamespaceFilters()
344
+ );
345
+ methodFilter.innerHTML = sectionHtml(
346
+ 'Controller Filters',
347
+ 'namespace-list methods-list',
348
+ renderControllerFlagFilters()
349
+ );
350
+ return;
351
+ }
352
+
353
+ if (currentView === 'models') {
354
+ namespaceFilter.style.opacity = '1';
355
+ namespaceFilter.style.pointerEvents = 'auto';
356
+ methodFilter.style.opacity = '1';
357
+ methodFilter.style.pointerEvents = 'auto';
358
+
359
+ namespaceFilter.innerHTML = sectionHtml(
360
+ 'Model Namespaces',
361
+ 'namespace-list',
362
+ renderModelNamespaceFilters()
363
+ );
364
+ methodFilter.innerHTML = sectionHtml(
365
+ 'Model Filters',
366
+ 'namespace-list methods-list',
367
+ renderModelFlagFilters()
368
+ );
369
+ return;
370
+ }
371
+
372
+ if (currentView === 'grpc') {
373
+ namespaceFilter.style.opacity = '1';
374
+ namespaceFilter.style.pointerEvents = 'auto';
375
+ methodFilter.style.opacity = '1';
376
+ methodFilter.style.pointerEvents = 'auto';
377
+
378
+ namespaceFilter.innerHTML = sectionHtml(
379
+ 'gRPC Namespaces',
380
+ 'namespace-list',
381
+ renderGrpcNamespaceFilters()
382
+ );
383
+ methodFilter.innerHTML = sectionHtml(
384
+ 'gRPC Filters',
385
+ 'namespace-list methods-list',
386
+ renderGrpcFlagFilters()
387
+ );
388
+ return;
389
+ }
390
+
391
+ // diagram: keep disabled (no meaningful sidebar filters)
392
+ namespaceFilter.style.opacity = '0.4';
393
+ namespaceFilter.style.pointerEvents = 'none';
394
+ methodFilter.style.opacity = '0.4';
395
+ methodFilter.style.pointerEvents = 'none';
396
+ }
397
+
398
+ function escapeHtml(s) {
399
+ return String(s)
400
+ .replace(/&/g, '&amp;')
401
+ .replace(/</g, '&lt;')
402
+ .replace(/>/g, '&gt;')
403
+ .replace(/"/g, '&quot;');
404
+ }
405
+
406
+ function renderFilterItem(label, count, filterType, filterValue, isActive) {
407
+ const safeLabel = escapeHtml(label);
408
+ const safeType = escapeHtml(filterType);
409
+ const safeValue = escapeHtml(filterValue);
410
+ return (
411
+ '<div class="namespace-item ' +
412
+ (isActive ? 'active' : '') +
413
+ '" data-filter-type="' +
414
+ safeType +
415
+ '" data-filter-value="' +
416
+ safeValue +
417
+ '">' +
418
+ '<span>' +
419
+ safeLabel +
420
+ '</span>' +
421
+ '<span class="namespace-count">' +
422
+ count +
423
+ '</span>' +
424
+ '</div>'
425
+ );
426
+ }
427
+
428
+ function renderRouteNamespaceFilters() {
429
+ const counts = new Map();
430
+ routes.forEach(r => {
431
+ const ns = r.namespace || '';
432
+ counts.set(ns, (counts.get(ns) || 0) + 1);
433
+ });
434
+ const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]);
435
+ const allCount = routes.length;
436
+ return [
437
+ renderFilterItem('All', allCount, 'routeNamespace', 'all', selectedNamespaces.has('all')),
438
+ ...entries.map(([ns, count]) =>
439
+ renderFilterItem(ns || 'root', count, 'routeNamespace', ns, selectedNamespaces.has(ns))
440
+ ),
441
+ ].join('');
442
+ }
443
+
444
+ function renderControllerNamespaceFilters() {
445
+ const counts = new Map();
446
+ controllers.forEach(c => {
447
+ const ns = c.namespace || '';
448
+ counts.set(ns, (counts.get(ns) || 0) + 1);
449
+ });
450
+ const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]);
451
+ const allCount = controllers.length;
452
+ return [
453
+ renderFilterItem('All', allCount, 'routeNamespace', 'all', selectedNamespaces.has('all')),
454
+ ...entries.map(([ns, count]) =>
455
+ renderFilterItem(ns || 'root', count, 'routeNamespace', ns, selectedNamespaces.has(ns))
456
+ ),
457
+ ].join('');
458
+ }
459
+
460
+ function renderRouteMethodFilters() {
461
+ const counts = new Map();
462
+ routes.forEach(r => {
463
+ const m = r.method || 'ALL';
464
+ counts.set(m, (counts.get(m) || 0) + 1);
465
+ });
466
+ const methods = ['ALL', 'GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
467
+ const allCount = routes.length;
468
+ return methods.map(m => {
469
+ const label = m === 'ALL' ? 'All' : m;
470
+ const value = m === 'ALL' ? 'all' : m;
471
+ const count = m === 'ALL' ? allCount : (counts.get(m) || 0);
472
+ const active = selectedMethods.has(value);
473
+ return renderFilterItem(label, count, 'routeMethod', value, active);
474
+ }).join('');
475
+ }
476
+
477
+ function renderControllerFlagFilters() {
478
+ const flags = [
479
+ {
480
+ key: 'json',
481
+ label: 'Renders JSON',
482
+ test: (c) => (c.actions || []).some((a) => a.rendersJson),
483
+ },
484
+ {
485
+ key: 'redirect',
486
+ label: 'Has Redirect',
487
+ test: (c) => (c.actions || []).some((a) => a.redirectsTo),
488
+ },
489
+ {
490
+ key: 'private',
491
+ label: 'Has Private Actions',
492
+ test: (c) => (c.actions || []).some((a) => a.visibility === 'private'),
493
+ },
494
+ ];
495
+ const allCount = controllers.length;
496
+ const items = [
497
+ renderFilterItem('All', allCount, 'controllerFlag', 'all', selectedControllerFlags.has('all')),
498
+ ];
499
+ flags.forEach(f => {
500
+ const count = controllers.filter(f.test).length;
501
+ items.push(renderFilterItem(f.label, count, 'controllerFlag', f.key, selectedControllerFlags.has(f.key)));
502
+ });
503
+ return items.join('');
504
+ }
505
+
506
+ function getModelNamespace(model) {
507
+ const p = (model.filePath || '').split('/');
508
+ if (p.length <= 1) return '';
509
+ return p.slice(0, -1).join('/');
510
+ }
511
+
512
+ function renderModelNamespaceFilters() {
513
+ const counts = new Map();
514
+ models.forEach(m => {
515
+ const ns = getModelNamespace(m);
516
+ counts.set(ns, (counts.get(ns) || 0) + 1);
517
+ });
518
+ const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]);
519
+ const allCount = models.length;
520
+ return [
521
+ renderFilterItem('All', allCount, 'modelNamespace', 'all', selectedModelNamespaces.has('all')),
522
+ ...entries.map(([ns, count]) =>
523
+ renderFilterItem(ns || 'root', count, 'modelNamespace', ns, selectedModelNamespaces.has(ns))
524
+ ),
525
+ ].join('');
526
+ }
527
+
528
+ function renderModelFlagFilters() {
529
+ const flags = [
530
+ { key: 'assoc', label: 'Has associations', test: (m) => (m.associations || []).length > 0 },
531
+ { key: 'valid', label: 'Has validations', test: (m) => (m.validations || []).length > 0 },
532
+ { key: 'cb', label: 'Has callbacks', test: (m) => (m.callbacks || []).length > 0 },
533
+ { key: 'concern', label: 'Includes concerns', test: (m) => (m.concerns || []).length > 0 },
534
+ { key: 'enum', label: 'Has enums', test: (m) => (m.enums || []).length > 0 },
535
+ ];
536
+ const allCount = models.length;
537
+ const items = [
538
+ renderFilterItem('All', allCount, 'modelFlag', 'all', selectedModelFlags.has('all')),
539
+ ];
540
+ flags.forEach(f => {
541
+ const count = models.filter(f.test).length;
542
+ items.push(renderFilterItem(f.label, count, 'modelFlag', f.key, selectedModelFlags.has(f.key)));
543
+ });
544
+ return items.join('');
545
+ }
546
+
547
+ function renderGrpcNamespaceFilters() {
548
+ const counts = new Map();
549
+ grpcServices.forEach(s => {
550
+ const ns = s.namespace || '';
551
+ counts.set(ns, (counts.get(ns) || 0) + 1);
552
+ });
553
+ const entries = [...counts.entries()].sort((a, b) => b[1] - a[1]);
554
+ const allCount = grpcServices.length;
555
+ return [
556
+ renderFilterItem('All', allCount, 'grpcNamespace', 'all', selectedGrpcNamespaces.has('all')),
557
+ ...entries.map(([ns, count]) =>
558
+ renderFilterItem(ns || 'root', count, 'grpcNamespace', ns, selectedGrpcNamespaces.has(ns))
559
+ ),
560
+ ].join('');
561
+ }
562
+
563
+ function renderGrpcFlagFilters() {
564
+ const flags = [
565
+ { key: 'policies', label: 'Has policies', test: (s) => (s.policies || []).length > 0 },
566
+ { key: 'serializers', label: 'Has serializers', test: (s) => (s.serializers || []).length > 0 },
567
+ { key: 'concerns', label: 'Includes concerns', test: (s) => (s.concerns || []).length > 0 },
568
+ { key: 'modelsUsed', label: 'RPC uses models', test: (s) => (s.rpcs || []).some(r => (r.modelsUsed || []).length > 0) },
569
+ { key: 'servicesUsed', label: 'RPC uses services', test: (s) => (s.rpcs || []).some(r => (r.servicesUsed || []).length > 0) },
570
+ ];
571
+ const allCount = grpcServices.length;
572
+ const items = [
573
+ renderFilterItem('All', allCount, 'grpcFlag', 'all', selectedGrpcFlags.has('all')),
574
+ ];
575
+ flags.forEach(f => {
576
+ const count = grpcServices.filter(f.test).length;
577
+ items.push(renderFilterItem(f.label, count, 'grpcFlag', f.key, selectedGrpcFlags.has(f.key)));
578
+ });
579
+ return items.join('');
580
+ }
581
+
288
582
  function filterRoutes() {
289
583
  return routes.filter(route => {
290
584
  // Namespace filter (multi-select)
@@ -355,6 +649,18 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
355
649
  if (!selectedNamespaces.has('all')) {
356
650
  filteredControllers = filteredControllers.filter(c => selectedNamespaces.has(c.namespace || ''));
357
651
  }
652
+ if (!selectedControllerFlags.has('all')) {
653
+ filteredControllers = filteredControllers.filter((c) => {
654
+ const rendersJson = (c.actions || []).some((a) => a.rendersJson);
655
+ const hasRedirect = (c.actions || []).some((a) => a.redirectsTo);
656
+ const hasPrivate = (c.actions || []).some((a) => a.visibility === 'private');
657
+
658
+ if (selectedControllerFlags.has('json') && !rendersJson) return false;
659
+ if (selectedControllerFlags.has('redirect') && !hasRedirect) return false;
660
+ if (selectedControllerFlags.has('private') && !hasPrivate) return false;
661
+ return true;
662
+ });
663
+ }
358
664
  const displayed = filteredControllers.slice(0, controllersDisplayCount);
359
665
  const hasMore = filteredControllers.length > controllersDisplayCount;
360
666
 
@@ -406,6 +712,27 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
406
712
  m.className.toLowerCase().includes(searchQuery)
407
713
  );
408
714
  }
715
+ // Model namespace filter
716
+ if (!selectedModelNamespaces.has('all')) {
717
+ filteredModels = filteredModels.filter(m => selectedModelNamespaces.has(getModelNamespace(m)));
718
+ }
719
+ // Model flag filter
720
+ if (!selectedModelFlags.has('all')) {
721
+ filteredModels = filteredModels.filter(m => {
722
+ const hasAssoc = (m.associations || []).length > 0;
723
+ const hasValid = (m.validations || []).length > 0;
724
+ const hasCb = (m.callbacks || []).length > 0;
725
+ const hasConcern = (m.concerns || []).length > 0;
726
+ const hasEnum = (m.enums || []).length > 0;
727
+
728
+ if (selectedModelFlags.has('assoc') && !hasAssoc) return false;
729
+ if (selectedModelFlags.has('valid') && !hasValid) return false;
730
+ if (selectedModelFlags.has('cb') && !hasCb) return false;
731
+ if (selectedModelFlags.has('concern') && !hasConcern) return false;
732
+ if (selectedModelFlags.has('enum') && !hasEnum) return false;
733
+ return true;
734
+ });
735
+ }
409
736
  const displayed = filteredModels.slice(0, modelsDisplayCount);
410
737
  const hasMore = filteredModels.length > modelsDisplayCount;
411
738
 
@@ -431,6 +758,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
431
758
  \${hasMore ? \`
432
759
  <div class="show-more-container">
433
760
  <button class="show-more-btn" onclick="loadMoreModels()">Show More (+50)</button>
761
+ <button class="show-more-btn" onclick="showAllModels()">Show All</button>
434
762
  <span class="show-more-count">\${modelsDisplayCount} / \${filteredModels.length}</span>
435
763
  </div>
436
764
  \` : ''}
@@ -442,6 +770,11 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
442
770
  renderMainPanel();
443
771
  };
444
772
 
773
+ window.showAllModels = function() {
774
+ modelsDisplayCount = filteredModels.length;
775
+ renderMainPanel();
776
+ };
777
+
445
778
  let grpcDisplayCount = 50;
446
779
  let filteredGrpc = grpcServices;
447
780
 
@@ -454,6 +787,25 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
454
787
  (svc.rpcs && svc.rpcs.some(rpc => rpc.name && rpc.name.toLowerCase().includes(searchQuery)))
455
788
  );
456
789
  }
790
+ if (!selectedGrpcNamespaces.has('all')) {
791
+ filteredGrpc = filteredGrpc.filter(svc => selectedGrpcNamespaces.has(svc.namespace || ''));
792
+ }
793
+ if (!selectedGrpcFlags.has('all')) {
794
+ filteredGrpc = filteredGrpc.filter(svc => {
795
+ const hasPolicies = (svc.policies || []).length > 0;
796
+ const hasSerializers = (svc.serializers || []).length > 0;
797
+ const hasConcerns = (svc.concerns || []).length > 0;
798
+ const usesModels = (svc.rpcs || []).some(r => (r.modelsUsed || []).length > 0);
799
+ const usesServices = (svc.rpcs || []).some(r => (r.servicesUsed || []).length > 0);
800
+
801
+ if (selectedGrpcFlags.has('policies') && !hasPolicies) return false;
802
+ if (selectedGrpcFlags.has('serializers') && !hasSerializers) return false;
803
+ if (selectedGrpcFlags.has('concerns') && !hasConcerns) return false;
804
+ if (selectedGrpcFlags.has('modelsUsed') && !usesModels) return false;
805
+ if (selectedGrpcFlags.has('servicesUsed') && !usesServices) return false;
806
+ return true;
807
+ });
808
+ }
457
809
 
458
810
  const displayedGrpc = filteredGrpc.slice(0, grpcDisplayCount);
459
811
 
@@ -524,10 +876,112 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
524
876
  detailPanel.classList.add('open');
525
877
  };
526
878
 
527
- function renderDiagramView() {
528
- const topModels = [...models]
879
+ // Diagram state
880
+ let diagramModelCount = 15;
881
+ let diagramNamespace = 'all';
882
+ let diagramFocusModel = '';
883
+ let diagramDepth = 2;
884
+
885
+ function getNamespaces() {
886
+ const ns = new Set();
887
+ const prefixes = new Map(); // Count common prefixes
888
+
889
+ models.forEach(m => {
890
+ const name = m.name || m.className || '';
891
+ // Check for Ruby namespace (::)
892
+ if (name.includes('::')) {
893
+ ns.add(name.split('::')[0]);
894
+ }
895
+ // Also try to find common prefixes (e.g., User, UserProfile, UserSetting -> User)
896
+ const match = name.match(/^([A-Z][a-z]+)/);
897
+ if (match) {
898
+ const prefix = match[1];
899
+ prefixes.set(prefix, (prefixes.get(prefix) || 0) + 1);
900
+ }
901
+ });
902
+
903
+ // Add prefixes that have 3+ models as pseudo-namespaces
904
+ prefixes.forEach((count, prefix) => {
905
+ if (count >= 3 && !ns.has(prefix)) {
906
+ ns.add(prefix + '*'); // Mark as prefix-based filter
907
+ }
908
+ });
909
+
910
+ return ['all', ...Array.from(ns).sort()];
911
+ }
912
+
913
+ function getModelNames() {
914
+ return models.map(m => m.name || m.className).sort();
915
+ }
916
+
917
+ // Get related models up to specified depth
918
+ function getRelatedModels(centerModel, depth) {
919
+ const related = new Set([centerModel]);
920
+ const modelMap = new Map();
921
+ models.forEach(m => {
922
+ const name = m.name || m.className;
923
+ modelMap.set(name, m);
924
+ });
925
+
926
+ for (let d = 0; d < depth; d++) {
927
+ const currentModels = [...related];
928
+ currentModels.forEach(modelName => {
929
+ const model = modelMap.get(modelName);
930
+ if (!model) return;
931
+
932
+ model.associations.forEach(assoc => {
933
+ const targetName = assoc.className || capitalize(singularize(assoc.name));
934
+ if (modelMap.has(targetName)) {
935
+ related.add(targetName);
936
+ }
937
+ });
938
+
939
+ // Also find models that reference this model
940
+ models.forEach(m => {
941
+ const mName = m.name || m.className;
942
+ m.associations.forEach(assoc => {
943
+ const targetName = assoc.className || capitalize(singularize(assoc.name));
944
+ if (targetName === modelName) {
945
+ related.add(mName);
946
+ }
947
+ });
948
+ });
949
+ });
950
+ }
951
+
952
+ return related;
953
+ }
954
+
955
+ function generateMermaidCode(modelCount, namespace, focusModel, depth) {
956
+ let filteredModels = [...models];
957
+
958
+ // Filter by focus model (takes priority)
959
+ if (focusModel && focusModel !== '') {
960
+ const relatedNames = getRelatedModels(focusModel, depth);
961
+ filteredModels = filteredModels.filter(m => {
962
+ const name = m.name || m.className;
963
+ return relatedNames.has(name);
964
+ });
965
+ }
966
+ // Filter by namespace
967
+ else if (namespace !== 'all') {
968
+ filteredModels = filteredModels.filter(m => {
969
+ const name = m.name || m.className || '';
970
+ // Handle prefix-based filter (ends with *)
971
+ if (namespace.endsWith('*')) {
972
+ const prefix = namespace.slice(0, -1);
973
+ return name.startsWith(prefix);
974
+ }
975
+ // Handle Ruby namespace (::)
976
+ return name.startsWith(namespace + '::') || name === namespace;
977
+ });
978
+ }
979
+
980
+ // Sort and limit
981
+ const count = modelCount === 'all' ? filteredModels.length : parseInt(modelCount) || 15;
982
+ const topModels = filteredModels
529
983
  .sort((a, b) => b.associations.length - a.associations.length)
530
- .slice(0, 15);
984
+ .slice(0, count);
531
985
 
532
986
  const modelNames = new Set(topModels.map(m => m.name || m.className));
533
987
  let mermaidCode = 'erDiagram\\n';
@@ -560,12 +1014,163 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
560
1014
  });
561
1015
  }
562
1016
 
1017
+ return { mermaidCode, modelCount: topModels.length, totalModels: filteredModels.length };
1018
+ }
1019
+
1020
+ window.toggleCustomInput = function() {
1021
+ const countSelect = document.getElementById('model-count-select');
1022
+ const customWrapper = document.getElementById('custom-input-wrapper');
1023
+ if (countSelect.value === 'custom') {
1024
+ customWrapper.style.display = 'flex';
1025
+ document.getElementById('model-count-input').focus();
1026
+ } else {
1027
+ customWrapper.style.display = 'none';
1028
+ document.getElementById('model-count-input').value = '';
1029
+ updateDiagram();
1030
+ }
1031
+ };
1032
+
1033
+ window.clearFocusModel = function() {
1034
+ document.getElementById('focus-model-select').value = '';
1035
+ diagramFocusModel = '';
1036
+ updateDiagram();
1037
+ };
1038
+
1039
+ window.updateDiagram = function() {
1040
+ const countInput = document.getElementById('model-count-input');
1041
+ const countSelect = document.getElementById('model-count-select');
1042
+ const nsSelect = document.getElementById('namespace-select');
1043
+ const focusSelect = document.getElementById('focus-model-select');
1044
+ const depthSelect = document.getElementById('depth-select');
1045
+
1046
+ // Get count from input or select
1047
+ let count;
1048
+ if (countSelect && countSelect.value === 'custom') {
1049
+ count = countInput ? countInput.value.trim() || '15' : '15';
1050
+ } else {
1051
+ count = countSelect ? countSelect.value : '15';
1052
+ }
1053
+ diagramModelCount = count;
1054
+ diagramNamespace = nsSelect ? nsSelect.value : 'all';
1055
+ diagramFocusModel = focusSelect ? focusSelect.value : '';
1056
+ diagramDepth = depthSelect ? parseInt(depthSelect.value) || 2 : 2;
1057
+
1058
+ // If focus model is set, disable namespace filter and enable depth
1059
+ if (nsSelect) {
1060
+ nsSelect.disabled = diagramFocusModel !== '';
1061
+ nsSelect.style.opacity = diagramFocusModel !== '' ? '0.5' : '1';
1062
+ }
1063
+ if (depthSelect) {
1064
+ depthSelect.disabled = diagramFocusModel === '';
1065
+ depthSelect.style.opacity = diagramFocusModel !== '' ? '1' : '0.5';
1066
+ const depthLabel = depthSelect.parentElement?.querySelector('span');
1067
+ if (depthLabel) {
1068
+ depthLabel.style.opacity = diagramFocusModel !== '' ? '1' : '0.5';
1069
+ }
1070
+ }
1071
+
1072
+ const { mermaidCode, modelCount, totalModels } = generateMermaidCode(count, diagramNamespace, diagramFocusModel, diagramDepth);
1073
+
1074
+ // Update diagram - need to recreate SVG
1075
+ const container = document.getElementById('mermaid-container');
1076
+ const diagram = document.getElementById('mermaid-diagram');
1077
+ if (diagram && window.mermaid) {
1078
+ // Remove old SVG
1079
+ const oldSvg = container.querySelector('svg');
1080
+ if (oldSvg) oldSvg.remove();
1081
+
1082
+ // Update mermaid code
1083
+ diagram.textContent = mermaidCode;
1084
+ diagram.removeAttribute('data-processed');
1085
+ diagram.style.display = 'block';
1086
+
1087
+ // Re-render
1088
+ window.mermaid.init(undefined, diagram);
1089
+ setTimeout(() => {
1090
+ initDiagramPanZoom();
1091
+ }, 200);
1092
+ }
1093
+
1094
+ // Update title
1095
+ const title = document.querySelector('.diagram-title-text');
1096
+ if (title) {
1097
+ let filterText = '';
1098
+ if (diagramFocusModel) {
1099
+ filterText = \` around \${diagramFocusModel} (depth \${diagramDepth})\`;
1100
+ } else if (diagramNamespace !== 'all') {
1101
+ filterText = \` in \${diagramNamespace}\`;
1102
+ }
1103
+ title.textContent = \`Model Relationships (\${modelCount}/\${totalModels} models\${filterText})\`;
1104
+ }
1105
+ };
1106
+
1107
+ function renderDiagramView() {
1108
+ const namespaces = getNamespaces();
1109
+ const modelNames = getModelNames();
1110
+ const { mermaidCode, modelCount, totalModels } = generateMermaidCode(diagramModelCount, diagramNamespace, diagramFocusModel, diagramDepth);
1111
+
1112
+ let filterText = '';
1113
+ if (diagramFocusModel) {
1114
+ filterText = \` around \${diagramFocusModel} (depth \${diagramDepth})\`;
1115
+ } else if (diagramNamespace !== 'all') {
1116
+ filterText = \` in \${diagramNamespace}\`;
1117
+ }
1118
+
1119
+ const isCustom = !['15', '30', '50', '100', 'all'].includes(String(diagramModelCount));
1120
+
563
1121
  return \`
564
- <div class="panel-header">
565
- <div class="panel-title">Model Relationships (Top 15 by associations)</div>
566
- </div>
567
- <div class="mermaid-container" id="mermaid-container">
568
- <pre class="mermaid" id="mermaid-diagram">\${mermaidCode}</pre>
1122
+ <div class="diagram-view-wrapper" style="display:flex;flex-direction:column;height:100%;min-height:0;">
1123
+ <div class="panel-header" style="flex-wrap:wrap;gap:8px;flex-shrink:0;">
1124
+ <div class="panel-title diagram-title-text">Model Relationships (\${modelCount}/\${totalModels} models\${filterText})</div>
1125
+ <div class="diagram-filters" style="display:flex;gap:12px;align-items:center;flex-wrap:wrap;font-size:12px;">
1126
+ <label style="display:flex;align-items:center;gap:6px;">
1127
+ <span>Limit:</span>
1128
+ <select id="model-count-select" onchange="toggleCustomInput()" style="padding:6px 10px;border-radius:4px;background:#2d2d2d;color:#fff;border:1px solid #444;min-width:80px;">
1129
+ <option value="15" \${diagramModelCount == 15 ? 'selected' : ''}>15</option>
1130
+ <option value="30" \${diagramModelCount == 30 ? 'selected' : ''}>30</option>
1131
+ <option value="50" \${diagramModelCount == 50 ? 'selected' : ''}>50</option>
1132
+ <option value="100" \${diagramModelCount == 100 ? 'selected' : ''}>100</option>
1133
+ <option value="all" \${diagramModelCount === 'all' ? 'selected' : ''}>All (\${models.length})</option>
1134
+ <option value="custom" \${isCustom ? 'selected' : ''}>Custom...</option>
1135
+ </select>
1136
+ <div id="custom-input-wrapper" style="display:\${isCustom ? 'flex' : 'none'};align-items:center;gap:4px;">
1137
+ <input type="number" id="model-count-input" placeholder="Enter number" min="1" max="\${models.length}"
1138
+ value="\${isCustom ? diagramModelCount : ''}"
1139
+ style="width:100px;padding:6px 10px;border-radius:4px;background:#2d2d2d;color:#fff;border:1px solid #444;"
1140
+ onchange="updateDiagram()" onkeyup="if(event.key==='Enter')updateDiagram()">
1141
+ <button onclick="updateDiagram()" style="padding:6px 12px;border-radius:4px;background:#3b82f6;color:#fff;border:none;cursor:pointer;">Apply</button>
1142
+ </div>
1143
+ </label>
1144
+ <label style="display:flex;align-items:center;gap:6px;">
1145
+ <span>Namespace:</span>
1146
+ <select id="namespace-select" onchange="updateDiagram()" style="padding:6px 10px;border-radius:4px;background:#2d2d2d;color:#fff;border:1px solid #444;\${diagramFocusModel ? 'opacity:0.5;' : ''}" \${diagramFocusModel ? 'disabled' : ''}>
1147
+ <option value="all" \${diagramNamespace === 'all' ? 'selected' : ''}>All</option>
1148
+ \${namespaces.filter(ns => ns !== 'all').map(ns => \`<option value="\${ns}" \${diagramNamespace === ns ? 'selected' : ''}>\${ns}</option>\`).join('')}
1149
+ </select>
1150
+ </label>
1151
+ <label style="display:flex;align-items:center;gap:6px;">
1152
+ <span>Focus:</span>
1153
+ <select id="focus-model-select" onchange="updateDiagram()" style="padding:6px 10px;border-radius:4px;background:#2d2d2d;color:#fff;border:1px solid #444;max-width:150px;">
1154
+ <option value="">None</option>
1155
+ \${modelNames.map(name => \`<option value="\${name}" \${diagramFocusModel === name ? 'selected' : ''}>\${name}</option>\`).join('')}
1156
+ </select>
1157
+ \${diagramFocusModel ? \`<button onclick="clearFocusModel()" style="padding:4px 8px;border-radius:4px;background:#666;color:#fff;border:none;cursor:pointer;" title="Clear focus">\u2715</button>\` : ''}
1158
+ </label>
1159
+ <label style="display:flex;align-items:center;gap:6px;">
1160
+ <span style="opacity:\${diagramFocusModel ? 1 : 0.5}">Depth:</span>
1161
+ <select id="depth-select" onchange="updateDiagram()" \${diagramFocusModel ? '' : 'disabled'} style="padding:6px 10px;border-radius:4px;background:#2d2d2d;color:#fff;border:1px solid #444;opacity:\${diagramFocusModel ? 1 : 0.5}">
1162
+ <option value="1" \${diagramDepth === 1 ? 'selected' : ''}>1</option>
1163
+ <option value="2" \${diagramDepth === 2 ? 'selected' : ''}>2</option>
1164
+ <option value="3" \${diagramDepth === 3 ? 'selected' : ''}>3</option>
1165
+ <option value="4" \${diagramDepth === 4 ? 'selected' : ''}>4</option>
1166
+ <option value="5" \${diagramDepth === 5 ? 'selected' : ''}>5</option>
1167
+ </select>
1168
+ </label>
1169
+ </div>
1170
+ </div>
1171
+ <div class="mermaid-container" id="mermaid-container" style="flex:1;min-height:0;">
1172
+ <pre class="mermaid" id="mermaid-diagram">\${mermaidCode}</pre>
1173
+ </div>
569
1174
  </div>
570
1175
  \`;
571
1176
  }
@@ -609,6 +1214,20 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
609
1214
  const svg = container?.querySelector('svg');
610
1215
  if (!svg) return;
611
1216
 
1217
+ // Calculate dynamic max zoom based on SVG size
1218
+ const svgRect = svg.getBoundingClientRect();
1219
+ const containerRect = container.getBoundingClientRect();
1220
+ const svgWidth = svgRect.width || 1000;
1221
+ const svgHeight = svgRect.height || 500;
1222
+
1223
+ // Max zoom: allow reading small text clearly
1224
+ // For very wide diagrams (many models), need much higher zoom
1225
+ const minZoom = 0.01;
1226
+ const maxZoom = Math.max(100, Math.ceil(svgWidth / 20)); // Very aggressive zoom
1227
+ window.diagramMaxZoom = maxZoom;
1228
+ window.diagramMinZoom = minZoom;
1229
+ console.log('Diagram zoom range:', minZoom, '-', maxZoom, 'x (SVG width:', svgWidth, 'px)');
1230
+
612
1231
  let scale = 1;
613
1232
  let translateX = 0;
614
1233
  let translateY = 0;
@@ -803,11 +1422,13 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
803
1422
  svg.style.transform = \`translate(\${translateX}px, \${translateY}px) scale(\${scale})\`;
804
1423
  }
805
1424
 
806
- // Mouse wheel zoom
1425
+ // Mouse wheel zoom (extended range: 0.3x to 10x)
807
1426
  container.addEventListener('wheel', (e) => {
808
1427
  e.preventDefault();
809
- const delta = e.deltaY > 0 ? -0.1 : 0.1;
810
- scale = Math.max(0.3, Math.min(3, scale + delta));
1428
+ // Dynamic step: larger steps at higher zoom levels for faster navigation
1429
+ const step = Math.max(0.1, scale * 0.15);
1430
+ const delta = e.deltaY > 0 ? -step : step;
1431
+ scale = Math.max(minZoom, Math.min(maxZoom, scale + delta));
811
1432
  updateTransform();
812
1433
  }, { passive: false });
813
1434
 
@@ -834,7 +1455,7 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
834
1455
  e.touches[0].clientY - e.touches[1].clientY
835
1456
  );
836
1457
  const delta = (dist - lastTouchDist) * 0.01;
837
- scale = Math.max(0.3, Math.min(3, scale + delta));
1458
+ scale = Math.max(minZoom, Math.min(maxZoom, scale + delta));
838
1459
  lastTouchDist = dist;
839
1460
  updateTransform();
840
1461
  } else if (e.touches.length === 1 && isDragging) {
@@ -873,7 +1494,10 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
873
1494
 
874
1495
  // Global functions for controls
875
1496
  window.diagramZoom = (delta) => {
876
- scale = Math.max(0.3, Math.min(3, scale + delta));
1497
+ // Dynamic step based on current scale
1498
+ const step = Math.max(0.2, scale * 0.2);
1499
+ const actualDelta = delta > 0 ? step : -step;
1500
+ scale = Math.max(minZoom, Math.min(maxZoom, scale + actualDelta));
877
1501
  updateTransform();
878
1502
  };
879
1503
 
@@ -1147,5 +1771,5 @@ import {k}from'./chunk-H7VVRHQZ.js';import*as d from'fs';import*as c from'path';
1147
1771
  `).join("")}
1148
1772
  </tbody>
1149
1773
  </table>
1150
- `}highlightParams(t){return t.replace(/:([a-zA-Z_]+)/g,'<span class="param">:$1</span>')}};async function m(){let n=process.argv[2]||process.cwd(),t=process.argv[3]||c.join(n,"rails-map.html");await new o(n).generate({title:"Rails Application Map",outputPath:t});}var p=import.meta.url===`file://${process.argv[1]}`;p&&m().catch(console.error);
1151
- export{o as a};
1774
+ `}highlightParams(t){return t.replace(/:([a-zA-Z_]+)/g,'<span class="param">:$1</span>')}};async function m(){let o=process.argv[2]||process.cwd(),t=process.argv[3]||d.join(o,"rails-map.html");await new i(o).generate({title:"Rails Application Map",outputPath:t});}var p=import.meta.url===`file://${process.argv[1]}`;p&&m().catch(console.error);
1775
+ export{i as a};