claude-crap 0.3.6 → 0.3.8

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.
Files changed (88) hide show
  1. package/README.md +25 -0
  2. package/dist/adapters/common.d.ts +1 -1
  3. package/dist/adapters/common.d.ts.map +1 -1
  4. package/dist/adapters/common.js +1 -1
  5. package/dist/adapters/common.js.map +1 -1
  6. package/dist/adapters/dart-analyzer.d.ts +41 -0
  7. package/dist/adapters/dart-analyzer.d.ts.map +1 -0
  8. package/dist/adapters/dart-analyzer.js +120 -0
  9. package/dist/adapters/dart-analyzer.js.map +1 -0
  10. package/dist/adapters/index.d.ts +1 -0
  11. package/dist/adapters/index.d.ts.map +1 -1
  12. package/dist/adapters/index.js +4 -0
  13. package/dist/adapters/index.js.map +1 -1
  14. package/dist/crap-config.d.ts +2 -0
  15. package/dist/crap-config.d.ts.map +1 -1
  16. package/dist/crap-config.js +36 -28
  17. package/dist/crap-config.js.map +1 -1
  18. package/dist/dashboard/file-detail.d.ts +77 -0
  19. package/dist/dashboard/file-detail.d.ts.map +1 -0
  20. package/dist/dashboard/file-detail.js +120 -0
  21. package/dist/dashboard/file-detail.js.map +1 -0
  22. package/dist/dashboard/server.d.ts +5 -0
  23. package/dist/dashboard/server.d.ts.map +1 -1
  24. package/dist/dashboard/server.js +103 -1
  25. package/dist/dashboard/server.js.map +1 -1
  26. package/dist/index.js +36 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/metrics/workspace-walker.d.ts +4 -1
  29. package/dist/metrics/workspace-walker.d.ts.map +1 -1
  30. package/dist/metrics/workspace-walker.js +12 -28
  31. package/dist/metrics/workspace-walker.js.map +1 -1
  32. package/dist/scanner/auto-scan.d.ts +9 -1
  33. package/dist/scanner/auto-scan.d.ts.map +1 -1
  34. package/dist/scanner/auto-scan.js +27 -5
  35. package/dist/scanner/auto-scan.js.map +1 -1
  36. package/dist/scanner/bootstrap.d.ts +1 -1
  37. package/dist/scanner/bootstrap.d.ts.map +1 -1
  38. package/dist/scanner/bootstrap.js +9 -0
  39. package/dist/scanner/bootstrap.js.map +1 -1
  40. package/dist/scanner/complexity-scanner.d.ts +56 -0
  41. package/dist/scanner/complexity-scanner.d.ts.map +1 -0
  42. package/dist/scanner/complexity-scanner.js +161 -0
  43. package/dist/scanner/complexity-scanner.js.map +1 -0
  44. package/dist/scanner/detector.d.ts +24 -4
  45. package/dist/scanner/detector.d.ts.map +1 -1
  46. package/dist/scanner/detector.js +105 -10
  47. package/dist/scanner/detector.js.map +1 -1
  48. package/dist/scanner/runner.d.ts +4 -1
  49. package/dist/scanner/runner.d.ts.map +1 -1
  50. package/dist/scanner/runner.js +12 -3
  51. package/dist/scanner/runner.js.map +1 -1
  52. package/dist/schemas/tool-schemas.d.ts +1 -1
  53. package/dist/schemas/tool-schemas.js +1 -1
  54. package/dist/schemas/tool-schemas.js.map +1 -1
  55. package/dist/shared/exclusions.d.ts +53 -0
  56. package/dist/shared/exclusions.d.ts.map +1 -0
  57. package/dist/shared/exclusions.js +126 -0
  58. package/dist/shared/exclusions.js.map +1 -0
  59. package/package.json +3 -1
  60. package/plugin/.claude-plugin/plugin.json +1 -1
  61. package/plugin/bundle/dashboard/public/index.html +432 -12
  62. package/plugin/bundle/mcp-server.mjs +747 -137
  63. package/plugin/bundle/mcp-server.mjs.map +4 -4
  64. package/plugin/package-lock.json +15 -2
  65. package/plugin/package.json +2 -1
  66. package/scripts/bundle-plugin.mjs +2 -1
  67. package/src/adapters/common.ts +1 -1
  68. package/src/adapters/dart-analyzer.ts +161 -0
  69. package/src/adapters/index.ts +4 -0
  70. package/src/crap-config.ts +55 -18
  71. package/src/dashboard/file-detail.ts +195 -0
  72. package/src/dashboard/public/index.html +432 -12
  73. package/src/dashboard/server.ts +140 -1
  74. package/src/index.ts +37 -4
  75. package/src/metrics/workspace-walker.ts +15 -27
  76. package/src/scanner/auto-scan.ts +41 -4
  77. package/src/scanner/bootstrap.ts +11 -0
  78. package/src/scanner/complexity-scanner.ts +222 -0
  79. package/src/scanner/detector.ts +114 -10
  80. package/src/scanner/runner.ts +12 -2
  81. package/src/schemas/tool-schemas.ts +1 -1
  82. package/src/shared/exclusions.ts +156 -0
  83. package/src/tests/adapters/dispatch.test.ts +2 -2
  84. package/src/tests/auto-scan.test.ts +2 -2
  85. package/src/tests/complexity-scanner.test.ts +263 -0
  86. package/src/tests/exclusions.test.ts +117 -0
  87. package/src/tests/file-detail-api.test.ts +258 -0
  88. package/src/tests/scanner-detector.test.ts +31 -11
@@ -192,6 +192,131 @@
192
192
  font-size: 12px;
193
193
  overflow-x: auto;
194
194
  }
195
+ .file-link {
196
+ cursor: pointer;
197
+ color: var(--accent);
198
+ }
199
+ .file-link:hover {
200
+ text-decoration: underline;
201
+ }
202
+ .back-link {
203
+ display: inline-flex;
204
+ align-items: center;
205
+ gap: 6px;
206
+ color: var(--accent);
207
+ font-size: 13px;
208
+ cursor: pointer;
209
+ margin-bottom: 16px;
210
+ }
211
+ .back-link:hover { text-decoration: underline; }
212
+ .file-header {
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 12px;
216
+ margin-bottom: 24px;
217
+ flex-wrap: wrap;
218
+ }
219
+ .file-header h2 {
220
+ margin: 0;
221
+ font-size: 18px;
222
+ font-weight: 600;
223
+ word-break: break-all;
224
+ }
225
+ .lang-badge {
226
+ display: inline-block;
227
+ padding: 2px 10px;
228
+ border-radius: 999px;
229
+ font-size: 11px;
230
+ font-weight: 600;
231
+ text-transform: uppercase;
232
+ letter-spacing: 0.04em;
233
+ background: rgba(62, 166, 255, 0.15);
234
+ color: var(--accent);
235
+ }
236
+ .grid.file-summary {
237
+ grid-template-columns: repeat(4, minmax(0, 1fr));
238
+ margin-bottom: 8px;
239
+ }
240
+ @media (max-width: 880px) {
241
+ .grid.file-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
242
+ }
243
+ .card .stat-value {
244
+ font-size: 28px;
245
+ font-weight: 700;
246
+ margin: 4px 0 8px 0;
247
+ font-variant-numeric: tabular-nums;
248
+ }
249
+ .source-view {
250
+ background: var(--card);
251
+ border: 1px solid var(--border);
252
+ border-radius: 10px;
253
+ overflow-x: auto;
254
+ font-family: "SF Mono", "Fira Code", "Consolas", monospace;
255
+ font-size: 12px;
256
+ line-height: 1.6;
257
+ }
258
+ .source-line {
259
+ display: flex;
260
+ min-height: 20px;
261
+ border-bottom: 1px solid transparent;
262
+ }
263
+ .source-line:hover {
264
+ background: rgba(255, 255, 255, 0.03);
265
+ }
266
+ .line-error {
267
+ background: rgba(231, 76, 60, 0.12);
268
+ }
269
+ .line-warning {
270
+ background: rgba(241, 196, 15, 0.08);
271
+ }
272
+ .line-note {
273
+ background: rgba(62, 166, 255, 0.08);
274
+ }
275
+ .line-fn-start {
276
+ border-top: 1px solid rgba(46, 204, 113, 0.25);
277
+ }
278
+ .line-number {
279
+ flex-shrink: 0;
280
+ width: 56px;
281
+ padding: 0 12px 0 12px;
282
+ text-align: right;
283
+ color: var(--muted);
284
+ user-select: none;
285
+ opacity: 0.6;
286
+ }
287
+ .line-gutter {
288
+ flex-shrink: 0;
289
+ width: 28px;
290
+ text-align: center;
291
+ padding: 0 2px;
292
+ }
293
+ .gutter-marker {
294
+ display: inline-block;
295
+ width: 18px;
296
+ height: 18px;
297
+ border-radius: 50%;
298
+ font-size: 10px;
299
+ font-weight: 700;
300
+ line-height: 18px;
301
+ text-align: center;
302
+ color: #0b0d10;
303
+ }
304
+ .gutter-error { background: var(--rating-E); }
305
+ .gutter-warning { background: var(--rating-C); }
306
+ .gutter-note { background: var(--accent); }
307
+ .line-code {
308
+ flex: 1;
309
+ padding: 0 16px 0 4px;
310
+ white-space: pre;
311
+ tab-size: 4;
312
+ }
313
+ .line-fn-label {
314
+ color: var(--rating-A);
315
+ font-size: 10px;
316
+ font-weight: 600;
317
+ opacity: 0.7;
318
+ margin-left: 12px;
319
+ }
195
320
  </style>
196
321
  </head>
197
322
  <body>
@@ -209,11 +334,129 @@
209
334
  </header>
210
335
 
211
336
  <main>
212
- <div v-if="loading" class="empty">Loading project score…</div>
213
- <div v-else-if="error" class="empty">
337
+ <!-- ═══ FILE DETAIL VIEW ═══ -->
338
+ <template v-if="currentView === 'file-detail' && fileDetail">
339
+ <div class="back-link" @click="goBack()">&#8592; Back to dashboard</div>
340
+ <div class="file-header">
341
+ <h2>{{ fileDetail.filePath }}</h2>
342
+ <span v-if="fileDetail.language" class="lang-badge">{{ fileDetail.language }}</span>
343
+ </div>
344
+
345
+ <!-- Summary cards -->
346
+ <div class="grid file-summary">
347
+ <div class="card">
348
+ <h2>Lines of Code</h2>
349
+ <div class="stat-value">{{ fileDetail.physicalLoc }}</div>
350
+ <div class="detail">{{ fileDetail.logicalLoc }} logical lines</div>
351
+ </div>
352
+ <div class="card">
353
+ <h2>Functions</h2>
354
+ <div class="stat-value">{{ fileDetail.functions.length }}</div>
355
+ <div class="detail">
356
+ Avg CC: {{ fileDetail.summary.avgComplexity }} · Max CC: {{ fileDetail.summary.maxComplexity }}
357
+ </div>
358
+ </div>
359
+ <div class="card">
360
+ <h2>Findings</h2>
361
+ <div class="stat-value" :style="{ color: fileDetail.summary.totalFindings > 0 ? 'var(--rating-D)' : 'var(--rating-A)' }">
362
+ {{ fileDetail.summary.totalFindings }}
363
+ </div>
364
+ <div class="detail">
365
+ {{ fileDetail.summary.errorCount }} error · {{ fileDetail.summary.warningCount }} warning · {{ fileDetail.summary.noteCount }} note
366
+ </div>
367
+ </div>
368
+ <div class="card">
369
+ <h2>Effort</h2>
370
+ <div class="stat-value">{{ fileDetail.summary.totalEffortMinutes }}m</div>
371
+ <div class="detail">Estimated remediation</div>
372
+ </div>
373
+ </div>
374
+
375
+ <!-- Methods table -->
376
+ <div v-if="fileDetail.functions.length" class="section-title">Methods</div>
377
+ <div v-if="fileDetail.functions.length" class="card">
378
+ <table>
379
+ <thead>
380
+ <tr>
381
+ <th>Method</th>
382
+ <th style="text-align: right">Line</th>
383
+ <th style="text-align: right">CC</th>
384
+ <th style="text-align: right">Lines</th>
385
+ <th>Status</th>
386
+ </tr>
387
+ </thead>
388
+ <tbody>
389
+ <tr v-for="fn in sortedFunctions" :key="fn.startLine">
390
+ <td><code>{{ fn.name }}</code></td>
391
+ <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.startLine }}</td>
392
+ <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.cyclomaticComplexity }}</td>
393
+ <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.lineCount }}</td>
394
+ <td>
395
+ <span v-if="fn.cyclomaticComplexity >= fileDetail.cyclomaticMax * 2" class="pill pill-error">error</span>
396
+ <span v-else-if="fn.cyclomaticComplexity > fileDetail.cyclomaticMax" class="pill pill-warning">warning</span>
397
+ <span v-else class="pill pill-note">ok</span>
398
+ </td>
399
+ </tr>
400
+ </tbody>
401
+ </table>
402
+ </div>
403
+
404
+ <!-- Findings table -->
405
+ <div v-if="fileDetail.findings.length" class="section-title">Findings</div>
406
+ <div v-if="fileDetail.findings.length" class="card">
407
+ <table>
408
+ <thead>
409
+ <tr>
410
+ <th>Rule</th>
411
+ <th>Level</th>
412
+ <th>Message</th>
413
+ <th style="text-align: right">Line</th>
414
+ <th>Tool</th>
415
+ <th style="text-align: right">Effort</th>
416
+ </tr>
417
+ </thead>
418
+ <tbody>
419
+ <tr v-for="(f, i) in sortedFindings" :key="i">
420
+ <td><code>{{ f.ruleId }}</code></td>
421
+ <td>
422
+ <span :class="'pill pill-' + f.level">{{ f.level }}</span>
423
+ </td>
424
+ <td style="max-width: 360px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ f.message }}</td>
425
+ <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ f.startLine }}</td>
426
+ <td>{{ f.sourceTool }}</td>
427
+ <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ f.effortMinutes }}m</td>
428
+ </tr>
429
+ </tbody>
430
+ </table>
431
+ </div>
432
+
433
+ <!-- Annotated source code -->
434
+ <div class="section-title">Source</div>
435
+ <div class="source-view">
436
+ <div
437
+ v-for="(line, idx) in fileDetail.sourceLines"
438
+ :key="idx"
439
+ :class="['source-line', lineClass(idx + 1)]"
440
+ >
441
+ <div class="line-number">{{ idx + 1 }}</div>
442
+ <div class="line-gutter">
443
+ <span
444
+ v-if="lineFindings(idx + 1).length"
445
+ :class="['gutter-marker', gutterClass(idx + 1)]"
446
+ :title="lineFindings(idx + 1).map(f => f.ruleId + ': ' + f.message).join('\\n')"
447
+ >{{ lineFindings(idx + 1).length }}</span>
448
+ </div>
449
+ <div class="line-code">{{ line }}<span v-if="lineFnLabel(idx + 1)" class="line-fn-label">{{ lineFnLabel(idx + 1) }}</span></div>
450
+ </div>
451
+ </div>
452
+ </template>
453
+
454
+ <!-- ═══ DASHBOARD VIEW ═══ -->
455
+ <div v-else-if="currentView === 'dashboard' && loading" class="empty">Loading project score…</div>
456
+ <div v-else-if="currentView === 'dashboard' && error" class="empty">
214
457
  Failed to load score: <code>{{ error }}</code>
215
458
  </div>
216
- <template v-else-if="score">
459
+ <template v-else-if="currentView === 'dashboard' && score">
217
460
  <!-- Top summary cards -->
218
461
  <div class="grid summary">
219
462
  <div class="card">
@@ -283,7 +526,7 @@
283
526
  </thead>
284
527
  <tbody>
285
528
  <tr v-for="[file, count] in fileEntries" :key="file">
286
- <td><code>{{ file }}</code></td>
529
+ <td><code class="file-link" @click="navigateToFile(file)">{{ file }}</code></td>
287
530
  <td style="text-align: right">{{ count }}</td>
288
531
  </tr>
289
532
  </tbody>
@@ -291,6 +534,46 @@
291
534
  <div v-else class="empty">No findings in the consolidated SARIF report yet.</div>
292
535
  </div>
293
536
 
537
+ <!-- Complexity hotspots -->
538
+ <div class="section-title">Complexity Hotspots</div>
539
+ <div class="card">
540
+ <div v-if="complexity" style="margin-bottom: 12px; color: var(--muted); font-size: 13px;">
541
+ Threshold: <strong style="color: var(--fg);">{{ complexity.threshold }}</strong>
542
+ &nbsp;·&nbsp;
543
+ {{ complexity.totalFunctions }} functions analyzed
544
+ &nbsp;·&nbsp;
545
+ <span :style="{ color: complexity.violationCount > 0 ? 'var(--rating-D)' : 'var(--rating-A)' }">
546
+ {{ complexity.violationCount }} violation{{ complexity.violationCount !== 1 ? 's' : '' }}
547
+ </span>
548
+ </div>
549
+ <table v-if="complexity && complexity.topFunctions.length">
550
+ <thead>
551
+ <tr>
552
+ <th>File</th>
553
+ <th>Function</th>
554
+ <th style="text-align: right">CC</th>
555
+ <th style="text-align: right">Lines</th>
556
+ <th>Status</th>
557
+ </tr>
558
+ </thead>
559
+ <tbody>
560
+ <tr v-for="fn in complexity.topFunctions" :key="fn.filePath + ':' + fn.startLine">
561
+ <td><code class="file-link" @click="navigateToFile(fn.filePath)">{{ fn.filePath }}</code></td>
562
+ <td><code>{{ fn.name }}</code></td>
563
+ <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.cyclomaticComplexity }}</td>
564
+ <td style="text-align: right; font-variant-numeric: tabular-nums;">{{ fn.lineCount }}</td>
565
+ <td>
566
+ <span v-if="fn.cyclomaticComplexity >= complexity.threshold * 2" class="pill pill-error">error</span>
567
+ <span v-else-if="fn.cyclomaticComplexity > complexity.threshold" class="pill pill-warning">warning</span>
568
+ <span v-else class="pill pill-note">ok</span>
569
+ </td>
570
+ </tr>
571
+ </tbody>
572
+ </table>
573
+ <div v-else-if="complexity" class="empty">No functions analyzed yet.</div>
574
+ <div v-else class="empty">Loading complexity data…</div>
575
+ </div>
576
+
294
577
  <!-- Where to drill in -->
295
578
  <div class="section-title">Reports</div>
296
579
  <div class="card">
@@ -311,11 +594,18 @@
311
594
  </div>
312
595
 
313
596
  <script>
314
- const { createApp, ref, computed, onMounted } = Vue;
597
+ const { createApp, ref, computed, onMounted, onUnmounted, watch } = Vue;
315
598
 
316
599
  createApp({
317
600
  setup() {
601
+ // ── Navigation state ──
602
+ const currentView = ref("dashboard"); // "dashboard" | "file-detail"
603
+ const selectedFile = ref(null);
604
+ const fileDetail = ref(null);
605
+
606
+ // ── Dashboard state ──
318
607
  const score = ref(null);
608
+ const complexity = ref(null);
319
609
  const loading = ref(true);
320
610
  const error = ref(null);
321
611
 
@@ -331,14 +621,131 @@
331
621
  .slice(0, 10);
332
622
  });
333
623
 
334
- function formatTimestamp(iso) {
624
+ // ── File detail computed ──
625
+ const sortedFunctions = computed(() => {
626
+ if (!fileDetail.value) return [];
627
+ return [...fileDetail.value.functions].sort((a, b) => b.cyclomaticComplexity - a.cyclomaticComplexity);
628
+ });
629
+
630
+ const sortedFindings = computed(() => {
631
+ if (!fileDetail.value) return [];
632
+ return [...fileDetail.value.findings].sort((a, b) => a.startLine - b.startLine);
633
+ });
634
+
635
+ // Build a per-line lookup: line number → array of findings on that line
636
+ const findingsByLine = computed(() => {
637
+ const map = {};
638
+ if (!fileDetail.value) return map;
639
+ for (const f of fileDetail.value.findings) {
640
+ const start = f.startLine;
641
+ const end = f.endLine || start;
642
+ for (let ln = start; ln <= end; ln++) {
643
+ if (!map[ln]) map[ln] = [];
644
+ map[ln].push(f);
645
+ }
646
+ }
647
+ return map;
648
+ });
649
+
650
+ // Build a per-line lookup: line number → worst finding level
651
+ const worstLevelByLine = computed(() => {
652
+ const map = {};
653
+ const rank = { error: 3, warning: 2, note: 1 };
654
+ for (const [ln, findings] of Object.entries(findingsByLine.value)) {
655
+ let worst = 0;
656
+ let worstLevel = null;
657
+ for (const f of findings) {
658
+ const r = rank[f.level] || 0;
659
+ if (r > worst) { worst = r; worstLevel = f.level; }
660
+ }
661
+ map[ln] = worstLevel;
662
+ }
663
+ return map;
664
+ });
665
+
666
+ // Build a set of function start lines for labels
667
+ const fnStartLines = computed(() => {
668
+ const map = {};
669
+ if (!fileDetail.value) return map;
670
+ for (const fn of fileDetail.value.functions) {
671
+ map[fn.startLine] = fn.name + " (CC:" + fn.cyclomaticComplexity + ")";
672
+ }
673
+ return map;
674
+ });
675
+
676
+ function lineFindings(lineNum) {
677
+ return findingsByLine.value[lineNum] || [];
678
+ }
679
+
680
+ function lineClass(lineNum) {
681
+ const level = worstLevelByLine.value[lineNum];
682
+ if (level === "error") return "line-error";
683
+ if (level === "warning") return "line-warning";
684
+ if (level === "note") return "line-note";
685
+ if (fnStartLines.value[lineNum]) return "line-fn-start";
686
+ return "";
687
+ }
688
+
689
+ function gutterClass(lineNum) {
690
+ const level = worstLevelByLine.value[lineNum];
691
+ if (level === "error") return "gutter-error";
692
+ if (level === "warning") return "gutter-warning";
693
+ return "gutter-note";
694
+ }
695
+
696
+ function lineFnLabel(lineNum) {
697
+ return fnStartLines.value[lineNum] || "";
698
+ }
699
+
700
+ // ── Navigation ──
701
+ function navigateToFile(path) {
702
+ window.location.hash = "#file=" + encodeURIComponent(path);
703
+ }
704
+
705
+ function goBack() {
706
+ window.location.hash = "";
707
+ }
708
+
709
+ async function loadFileDetail(path) {
710
+ selectedFile.value = path;
711
+ currentView.value = "file-detail";
712
+ fileDetail.value = null;
335
713
  try {
336
- return new Date(iso).toLocaleString();
337
- } catch {
338
- return iso;
714
+ const res = await fetch("/api/file-detail?path=" + encodeURIComponent(path));
715
+ if (!res.ok) throw new Error("HTTP " + res.status);
716
+ fileDetail.value = await res.json();
717
+ } catch (err) {
718
+ fileDetail.value = null;
719
+ error.value = "Failed to load file detail: " + (err.message || err);
720
+ currentView.value = "dashboard";
339
721
  }
340
722
  }
341
723
 
724
+ function handleHashChange() {
725
+ const hash = window.location.hash;
726
+ if (hash.startsWith("#file=")) {
727
+ const path = decodeURIComponent(hash.substring(6));
728
+ loadFileDetail(path);
729
+ } else {
730
+ currentView.value = "dashboard";
731
+ selectedFile.value = null;
732
+ fileDetail.value = null;
733
+ }
734
+ }
735
+
736
+ // ── Dashboard data ──
737
+ function formatTimestamp(iso) {
738
+ try { return new Date(iso).toLocaleString(); }
739
+ catch { return iso; }
740
+ }
741
+
742
+ async function refreshComplexity() {
743
+ try {
744
+ const res = await fetch("/api/complexity");
745
+ if (res.ok) complexity.value = await res.json();
746
+ } catch { /* non-fatal */ }
747
+ }
748
+
342
749
  async function refresh() {
343
750
  loading.value = true;
344
751
  error.value = null;
@@ -351,16 +758,29 @@
351
758
  } finally {
352
759
  loading.value = false;
353
760
  }
761
+ refreshComplexity();
354
762
  }
355
763
 
356
764
  onMounted(() => {
357
765
  refresh();
358
- // Poll every 10s so the dashboard stays roughly in sync
359
- // with new ingestions without requiring a manual reload.
360
766
  setInterval(refresh, 10_000);
767
+ window.addEventListener("hashchange", handleHashChange);
768
+ // Check if page loaded with a hash already
769
+ handleHashChange();
770
+ });
771
+
772
+ onUnmounted(() => {
773
+ window.removeEventListener("hashchange", handleHashChange);
361
774
  });
362
775
 
363
- return { score, loading, error, toolEntries, fileEntries, formatTimestamp };
776
+ return {
777
+ currentView, selectedFile, fileDetail,
778
+ score, complexity, loading, error,
779
+ toolEntries, fileEntries, formatTimestamp,
780
+ sortedFunctions, sortedFindings,
781
+ navigateToFile, goBack,
782
+ lineFindings, lineClass, gutterClass, lineFnLabel,
783
+ };
364
784
  },
365
785
  }).mount("#app");
366
786
  </script>