archsight 0.1.2 → 0.1.3

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +26 -5
  3. data/lib/archsight/analysis/executor.rb +112 -0
  4. data/lib/archsight/analysis/result.rb +174 -0
  5. data/lib/archsight/analysis/sandbox.rb +319 -0
  6. data/lib/archsight/analysis.rb +11 -0
  7. data/lib/archsight/annotations/architecture_annotations.rb +2 -2
  8. data/lib/archsight/cli.rb +163 -0
  9. data/lib/archsight/database.rb +6 -2
  10. data/lib/archsight/helpers/analysis_renderer.rb +83 -0
  11. data/lib/archsight/helpers/formatting.rb +95 -0
  12. data/lib/archsight/helpers.rb +20 -4
  13. data/lib/archsight/import/concurrent_progress.rb +341 -0
  14. data/lib/archsight/import/executor.rb +466 -0
  15. data/lib/archsight/import/git_analytics.rb +626 -0
  16. data/lib/archsight/import/handler.rb +263 -0
  17. data/lib/archsight/import/handlers/github.rb +161 -0
  18. data/lib/archsight/import/handlers/gitlab.rb +202 -0
  19. data/lib/archsight/import/handlers/jira_base.rb +189 -0
  20. data/lib/archsight/import/handlers/jira_discover.rb +161 -0
  21. data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
  22. data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
  23. data/lib/archsight/import/handlers/repository.rb +439 -0
  24. data/lib/archsight/import/handlers/rest_api.rb +293 -0
  25. data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
  26. data/lib/archsight/import/progress.rb +91 -0
  27. data/lib/archsight/import/registry.rb +54 -0
  28. data/lib/archsight/import/shared_file_writer.rb +67 -0
  29. data/lib/archsight/import/team_matcher.rb +195 -0
  30. data/lib/archsight/import.rb +14 -0
  31. data/lib/archsight/resources/analysis.rb +91 -0
  32. data/lib/archsight/resources/application_component.rb +2 -2
  33. data/lib/archsight/resources/application_service.rb +12 -12
  34. data/lib/archsight/resources/business_product.rb +12 -12
  35. data/lib/archsight/resources/data_object.rb +1 -1
  36. data/lib/archsight/resources/import.rb +79 -0
  37. data/lib/archsight/resources/technology_artifact.rb +23 -2
  38. data/lib/archsight/version.rb +1 -1
  39. data/lib/archsight/web/api/docs.rb +17 -0
  40. data/lib/archsight/web/api/json_helpers.rb +164 -0
  41. data/lib/archsight/web/api/openapi/spec.yaml +500 -0
  42. data/lib/archsight/web/api/routes.rb +101 -0
  43. data/lib/archsight/web/application.rb +66 -43
  44. data/lib/archsight/web/doc/import.md +458 -0
  45. data/lib/archsight/web/doc/index.md.erb +1 -0
  46. data/lib/archsight/web/public/css/artifact.css +10 -0
  47. data/lib/archsight/web/public/css/graph.css +14 -0
  48. data/lib/archsight/web/public/css/instance.css +489 -0
  49. data/lib/archsight/web/views/api_docs.erb +19 -0
  50. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
  51. data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
  52. data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
  53. data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
  54. data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
  55. data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
  56. data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
  57. data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
  58. metadata +78 -1
@@ -327,3 +327,492 @@
327
327
  color: var(--muted-color);
328
328
  font-style: italic;
329
329
  }
330
+
331
+ /* Analysis resource styles */
332
+ .analysis-header {
333
+ margin-bottom: 0.75rem;
334
+ }
335
+
336
+ .analysis-header h2 {
337
+ display: flex;
338
+ align-items: center;
339
+ gap: 0.5rem;
340
+ }
341
+
342
+ .analysis-description {
343
+ margin-top: 0.75rem;
344
+ padding: 0.75rem;
345
+ background-color: var(--card-background-color);
346
+ border-radius: 8px;
347
+ border: 1px solid var(--muted-border-color);
348
+ }
349
+
350
+ .analysis-metadata {
351
+ display: flex;
352
+ flex-wrap: wrap;
353
+ gap: 1rem;
354
+ align-items: center;
355
+ padding: 0.75rem 0;
356
+ margin-top: 0.5rem;
357
+ }
358
+
359
+ .analysis-meta-item {
360
+ display: flex;
361
+ align-items: center;
362
+ gap: 0.35rem;
363
+ font-size: 0.9em;
364
+ color: var(--muted-color);
365
+ padding: 0.25rem 0.5rem;
366
+ background-color: var(--code-background-color);
367
+ border-radius: 4px;
368
+ }
369
+
370
+ .analysis-meta-item i {
371
+ font-size: 1.1em;
372
+ }
373
+
374
+ .analysis-meta-item.status-enabled {
375
+ color: #10b981;
376
+ }
377
+
378
+ .analysis-meta-item.status-disabled {
379
+ color: #ef4444;
380
+ }
381
+
382
+ .analysis-script pre.code {
383
+ margin: 0;
384
+ white-space: pre;
385
+ overflow-x: auto;
386
+ }
387
+
388
+ .analysis-script pre.code code {
389
+ white-space: pre;
390
+ }
391
+
392
+ .analysis-execution header {
393
+ display: flex;
394
+ justify-content: space-between;
395
+ align-items: center;
396
+ }
397
+
398
+ .analysis-execution header h3 {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 0.5rem;
402
+ margin: 0;
403
+ }
404
+
405
+ .analysis-execute-controls {
406
+ display: flex;
407
+ align-items: center;
408
+ gap: 0.75rem;
409
+ }
410
+
411
+ .analysis-execute-controls button {
412
+ display: flex;
413
+ align-items: center;
414
+ gap: 0.35rem;
415
+ padding: 0.4rem 0.75rem;
416
+ font-size: 0.85em;
417
+ margin: 0;
418
+ }
419
+
420
+ .analysis-loading {
421
+ display: flex;
422
+ align-items: center;
423
+ justify-content: center;
424
+ gap: 0.5rem;
425
+ padding: 2rem;
426
+ color: var(--muted-color);
427
+ }
428
+
429
+ .htmx-indicator {
430
+ display: none;
431
+ }
432
+
433
+ .htmx-request .htmx-indicator {
434
+ display: inline-flex;
435
+ align-items: center;
436
+ gap: 0.5rem;
437
+ color: var(--muted-color);
438
+ }
439
+
440
+ .htmx-request #execute-analysis-btn {
441
+ opacity: 0.6;
442
+ pointer-events: none;
443
+ }
444
+
445
+ @keyframes spin {
446
+ from { transform: rotate(0deg); }
447
+ to { transform: rotate(360deg); }
448
+ }
449
+
450
+ .spinning {
451
+ animation: spin 1s linear infinite;
452
+ }
453
+
454
+ .analysis-results {
455
+ min-height: 100px;
456
+ padding: 1rem;
457
+ background-color: var(--card-background-color);
458
+ border: 1px solid var(--muted-border-color);
459
+ border-radius: 8px;
460
+ }
461
+
462
+ .analysis-results-placeholder {
463
+ display: flex;
464
+ align-items: center;
465
+ gap: 0.5rem;
466
+ color: var(--muted-color);
467
+ font-style: italic;
468
+ margin: 0;
469
+ }
470
+
471
+ .analysis-empty-state {
472
+ display: flex;
473
+ align-items: center;
474
+ gap: 0.5rem;
475
+ padding: 1.5rem;
476
+ color: var(--muted-color);
477
+ }
478
+
479
+ /* Analysis result styles */
480
+ .analysis-result-container {
481
+ padding: 0;
482
+ }
483
+
484
+ .analysis-result-header {
485
+ display: flex;
486
+ justify-content: space-between;
487
+ align-items: center;
488
+ padding: 0.75rem 1rem;
489
+ margin: -1rem -1rem 1rem -1rem;
490
+ background-color: var(--code-background-color);
491
+ border-radius: 8px 8px 0 0;
492
+ border-bottom: 1px solid var(--muted-border-color);
493
+ }
494
+
495
+ .analysis-result-header .status-indicator {
496
+ display: flex;
497
+ align-items: center;
498
+ gap: 0.5rem;
499
+ font-weight: 600;
500
+ }
501
+
502
+ .analysis-result-header .status-success {
503
+ color: #10b981;
504
+ }
505
+
506
+ .analysis-result-header .status-findings {
507
+ color: #f59e0b;
508
+ }
509
+
510
+ .analysis-result-header .status-error {
511
+ color: #ef4444;
512
+ }
513
+
514
+ .analysis-result-header .duration {
515
+ display: flex;
516
+ align-items: center;
517
+ gap: 0.35rem;
518
+ font-size: 0.9em;
519
+ color: var(--muted-color);
520
+ }
521
+
522
+ .analysis-error-details {
523
+ padding: 0.75rem 1rem;
524
+ margin-bottom: 1rem;
525
+ background-color: rgba(239, 68, 68, 0.1);
526
+ border: 1px solid rgba(239, 68, 68, 0.3);
527
+ border-radius: 4px;
528
+ color: #ef4444;
529
+ }
530
+
531
+ .analysis-error-details details {
532
+ margin-top: 0.5rem;
533
+ }
534
+
535
+ .analysis-error-details summary {
536
+ cursor: pointer;
537
+ color: var(--muted-color);
538
+ font-size: 0.9em;
539
+ }
540
+
541
+ .analysis-error-details pre {
542
+ margin-top: 0.5rem;
543
+ font-size: 0.85em;
544
+ max-height: 200px;
545
+ overflow: auto;
546
+ }
547
+
548
+ .analysis-output {
549
+ margin-top: 0.5rem;
550
+ }
551
+
552
+ .analysis-heading {
553
+ font-weight: 600;
554
+ margin: 1rem 0 0.5rem 0;
555
+ }
556
+
557
+ .analysis-heading.level-0 {
558
+ font-size: 1.25em;
559
+ border-bottom: 1px solid var(--muted-border-color);
560
+ padding-bottom: 0.25rem;
561
+ }
562
+
563
+ .analysis-heading.level-1 {
564
+ font-size: 1.1em;
565
+ }
566
+
567
+ .analysis-heading.level-2 {
568
+ font-size: 1em;
569
+ }
570
+
571
+ .analysis-text {
572
+ margin: 0.5rem 0;
573
+ }
574
+
575
+ .analysis-text p {
576
+ margin: 0;
577
+ }
578
+
579
+ .analysis-message {
580
+ display: flex;
581
+ align-items: flex-start;
582
+ gap: 0.5rem;
583
+ padding: 0.5rem 0.75rem;
584
+ margin: 0.5rem 0;
585
+ border-radius: 4px;
586
+ }
587
+
588
+ .analysis-message i {
589
+ margin-top: 0.15rem;
590
+ }
591
+
592
+ .analysis-message.message-info {
593
+ background-color: rgba(59, 130, 246, 0.1);
594
+ border: 1px solid rgba(59, 130, 246, 0.3);
595
+ color: #3b82f6;
596
+ }
597
+
598
+ .analysis-message.message-warning {
599
+ background-color: rgba(245, 158, 11, 0.1);
600
+ border: 1px solid rgba(245, 158, 11, 0.3);
601
+ color: #f59e0b;
602
+ }
603
+
604
+ .analysis-message.message-error {
605
+ background-color: rgba(239, 68, 68, 0.1);
606
+ border: 1px solid rgba(239, 68, 68, 0.3);
607
+ color: #ef4444;
608
+ }
609
+
610
+ .analysis-table-wrapper {
611
+ margin: 0.75rem 0;
612
+ overflow-x: auto;
613
+ }
614
+
615
+ .analysis-list {
616
+ margin: 0.5rem 0;
617
+ padding-left: 1.5rem;
618
+ }
619
+
620
+ .analysis-list li {
621
+ margin: 0.25rem 0;
622
+ }
623
+
624
+ /* Analysis details collapsible section */
625
+ .analysis-details-section {
626
+ margin-top: 1rem;
627
+ border: 1px solid var(--muted-border-color);
628
+ border-radius: 8px;
629
+ background-color: var(--card-background-color);
630
+ }
631
+
632
+ .analysis-details-section summary {
633
+ display: flex;
634
+ align-items: center;
635
+ gap: 0.5rem;
636
+ padding: 0.75rem 1rem;
637
+ cursor: pointer;
638
+ font-weight: 600;
639
+ color: var(--muted-color);
640
+ user-select: none;
641
+ }
642
+
643
+ .analysis-details-section summary:hover {
644
+ color: var(--color);
645
+ }
646
+
647
+ .analysis-details-section summary::marker,
648
+ .analysis-details-section summary::-webkit-details-marker {
649
+ display: none;
650
+ }
651
+
652
+ .analysis-details-section summary::before {
653
+ content: '\25B6';
654
+ font-size: 0.7em;
655
+ transition: transform 0.2s ease;
656
+ }
657
+
658
+ .analysis-details-section[open] summary::before {
659
+ transform: rotate(90deg);
660
+ }
661
+
662
+ .analysis-details-section[open] {
663
+ padding-bottom: 0;
664
+ }
665
+
666
+ .analysis-details-content {
667
+ padding: 1rem;
668
+ border-top: 1px solid var(--muted-border-color);
669
+ }
670
+
671
+ .analysis-details-content .analysis-metadata {
672
+ margin-bottom: 1rem;
673
+ }
674
+
675
+ .analysis-details-content .analysis-script {
676
+ border: none;
677
+ margin: 0;
678
+ }
679
+
680
+ .analysis-script-header {
681
+ display: flex;
682
+ justify-content: space-between;
683
+ align-items: center;
684
+ margin-bottom: 0.5rem;
685
+ }
686
+
687
+ .analysis-script-header .copy-button {
688
+ padding: 0.25rem 0.5rem;
689
+ font-size: 0.85em;
690
+ }
691
+
692
+ /* Import detail styles */
693
+ .import-header {
694
+ margin-bottom: 0.75rem;
695
+ }
696
+
697
+ .import-header header {
698
+ display: flex;
699
+ flex-wrap: wrap;
700
+ align-items: center;
701
+ gap: 0.5rem;
702
+ }
703
+
704
+ .import-header header h2 {
705
+ display: flex;
706
+ align-items: center;
707
+ gap: 0.5rem;
708
+ margin-right: auto;
709
+ }
710
+
711
+ .import-description {
712
+ margin-top: 0.75rem;
713
+ padding: 0.75rem;
714
+ background-color: var(--card-background-color);
715
+ border-radius: 8px;
716
+ border: 1px solid var(--muted-border-color);
717
+ }
718
+
719
+ .import-metadata {
720
+ display: flex;
721
+ flex-wrap: wrap;
722
+ gap: 1rem;
723
+ margin-bottom: 1rem;
724
+ }
725
+
726
+ .import-meta-item {
727
+ display: flex;
728
+ align-items: center;
729
+ gap: 0.35rem;
730
+ font-size: 0.9em;
731
+ color: var(--muted-color);
732
+ padding: 0.25rem 0.5rem;
733
+ background-color: var(--code-background-color);
734
+ border-radius: 4px;
735
+ }
736
+
737
+ .import-meta-item i {
738
+ font-size: 1.1em;
739
+ }
740
+
741
+ .import-meta-item.status-enabled {
742
+ color: #10b981;
743
+ }
744
+
745
+ .import-meta-item.status-disabled {
746
+ color: #ef4444;
747
+ }
748
+
749
+ .import-output-path {
750
+ margin-bottom: 1rem;
751
+ padding: 0.5rem 0.75rem;
752
+ background-color: var(--code-background-color);
753
+ border-radius: 4px;
754
+ }
755
+
756
+ .import-output-path code {
757
+ margin-left: 0.5rem;
758
+ }
759
+
760
+ .import-config-section {
761
+ margin-top: 1rem;
762
+ border: 1px solid var(--muted-border-color);
763
+ border-radius: 8px;
764
+ background-color: var(--card-background-color);
765
+ }
766
+
767
+ .import-config-section summary {
768
+ display: flex;
769
+ align-items: center;
770
+ gap: 0.5rem;
771
+ padding: 0.75rem 1rem;
772
+ cursor: pointer;
773
+ font-weight: 600;
774
+ color: var(--muted-color);
775
+ user-select: none;
776
+ }
777
+
778
+ .import-config-section summary:hover {
779
+ color: var(--color);
780
+ }
781
+
782
+ .import-config-section summary::marker,
783
+ .import-config-section summary::-webkit-details-marker {
784
+ display: none;
785
+ }
786
+
787
+ .import-config-section summary::before {
788
+ content: '\25B6';
789
+ font-size: 0.7em;
790
+ transition: transform 0.2s ease;
791
+ }
792
+
793
+ .import-config-section[open] summary::before {
794
+ transform: rotate(90deg);
795
+ }
796
+
797
+ .import-config-table {
798
+ width: 100%;
799
+ margin: 0;
800
+ border-collapse: collapse;
801
+ }
802
+
803
+ .import-config-table th,
804
+ .import-config-table td {
805
+ padding: 0.5rem 1rem;
806
+ border-top: 1px solid var(--muted-border-color);
807
+ }
808
+
809
+ .import-config-table th {
810
+ width: 30%;
811
+ text-align: left;
812
+ font-weight: 500;
813
+ color: var(--muted-color);
814
+ }
815
+
816
+ .import-config-table code {
817
+ word-break: break-all;
818
+ }
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Archsight API Documentation</title>
5
+ <meta charset="utf-8"/>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
8
+ <style>
9
+ body {
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <redoc spec-url='/api/v1/openapi.yaml'></redoc>
17
+ <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
18
+ </body>
19
+ </html>
@@ -5,22 +5,28 @@
5
5
  - schedule = annotations['scc/estimatedScheduleMonths']
6
6
  - people = annotations["scc/estimatedPeople"]
7
7
  - if cost || schedule || people
8
+ - ai_cost = ai_adjusted_estimate(:cost, cost)
9
+ - ai_schedule = ai_adjusted_estimate(:schedule, schedule)
10
+ - ai_people = ai_adjusted_estimate(:team, people)
8
11
  %tr
9
- %th{ scope: "row" } Project Estimate
12
+ %th{ scope: "row" }
13
+ Project Estimate
14
+ %span.estimate-note{ title: "Estimates adjusted for \u20AC80k salary and AI-assisted development (3x productivity)" }
15
+ %i.iconoir-info-circle
10
16
  %td
11
17
  .project-estimate
12
- - if cost
18
+ - if ai_cost
13
19
  .estimate-item
14
- %i.iconoir-dollar-circle
20
+ %i.iconoir-euro
15
21
  %span.estimate-label Cost:
16
- %span.estimate-value= to_dollar(cost.to_f)
17
- - if schedule
22
+ %span.estimate-value= to_euro(ai_cost)
23
+ - if ai_schedule
18
24
  .estimate-item
19
25
  %i.iconoir-calendar
20
26
  %span.estimate-label Schedule:
21
- %span.estimate-value= "#{schedule} months"
22
- - if people
27
+ %span.estimate-value= "#{format('%.1f', ai_schedule)} months"
28
+ - if ai_people
23
29
  .estimate-item
24
30
  %i.iconoir-group
25
31
  %span.estimate-label Team:
26
- %span.estimate-value= "#{people} people"
32
+ %span.estimate-value= "#{ai_people} people"
@@ -0,0 +1,74 @@
1
+ - analysis = db.instance_by_kind(@kind, @instance)
2
+ - return unless analysis
3
+ - script = analysis.annotations['analysis/script']
4
+ - description = analysis.annotations['analysis/description']
5
+ - handler = analysis.annotations['analysis/handler'] || 'ruby'
6
+ - timeout = analysis.annotations['analysis/timeout'] || '30s'
7
+ - enabled = analysis.annotations['analysis/enabled'] != 'false'
8
+
9
+ %article.analysis-header
10
+ %header
11
+ %h2
12
+ %i{class: "iconoir-#{analysis.class.icon} icon-#{analysis.class.layer}"}
13
+ .instance-title-text
14
+ %span.instance-name= @instance
15
+ %span.instance-kind-subtitle= @kind
16
+ - unless enabled
17
+ %span.badge.badge-warning Disabled
18
+ - if description
19
+ .analysis-description
20
+ != markdown(description)
21
+
22
+ - if script
23
+ %article.analysis-execution
24
+ %header
25
+ %h3
26
+ %i.iconoir-play
27
+ Results
28
+ .analysis-execute-controls
29
+ %button#execute-analysis-btn.secondary.outline{"hx-post": "/kinds/Analysis/instances/#{@instance}/execute", "hx-target": "#analysis-results", "hx-swap": "innerHTML", "hx-indicator": "#execute-spinner"}
30
+ %i.iconoir-refresh
31
+ Re-run
32
+ %span#execute-spinner.htmx-indicator
33
+ %i.iconoir-refresh.spinning
34
+ Running...
35
+ #analysis-results.analysis-results{"hx-post": "/kinds/Analysis/instances/#{@instance}/execute", "hx-trigger": "load", "hx-swap": "innerHTML"}
36
+ .analysis-loading
37
+ %i.iconoir-refresh.spinning
38
+ Running analysis...
39
+
40
+ %details.analysis-details-section
41
+ %summary
42
+ %i.iconoir-code-brackets-square
43
+ Script Details
44
+ .analysis-details-content
45
+ .analysis-metadata
46
+ %span.analysis-meta-item
47
+ %i.iconoir-code
48
+ = handler
49
+ %span.analysis-meta-item
50
+ %i.iconoir-timer
51
+ = timeout
52
+ - if enabled
53
+ %span.analysis-meta-item.status-enabled
54
+ %i.iconoir-check-circle
55
+ Enabled
56
+ - else
57
+ %span.analysis-meta-item.status-disabled
58
+ %i.iconoir-xmark-circle
59
+ Disabled
60
+ .analysis-script
61
+ .analysis-script-header
62
+ %strong Script
63
+ %button.copy-button{onclick: "navigator.clipboard.writeText(document.getElementById('analysis-script-content').textContent)", title: "Copy script to clipboard"}
64
+ %i.iconoir-copy
65
+ %pre.code
66
+ %code#analysis-script-content.language-ruby= script
67
+
68
+ - else
69
+ %article
70
+ %p.analysis-empty-state
71
+ %i.iconoir-warning-triangle
72
+ No script defined for this analysis.
73
+
74
+ != haml :"partials/instance/_relations", locals: { instance: analysis }
@@ -0,0 +1,64 @@
1
+ - accordion_name = "analysis-#{result.name.gsub(/[^a-zA-Z0-9]/, '-')}"
2
+
3
+ .analysis-result-container{class: result.success? ? 'success' : 'failed'}
4
+ .analysis-result-header
5
+ %span.status-indicator
6
+ - if result.success?
7
+ - if result.has_findings?
8
+ %i.iconoir-warning-triangle.status-findings
9
+ Completed with findings
10
+ - else
11
+ %i.iconoir-check-circle.status-success
12
+ Completed successfully
13
+ - else
14
+ %i.iconoir-xmark-circle.status-error
15
+ Failed
16
+ - if result.duration
17
+ %span.duration
18
+ %i.iconoir-timer
19
+ = format("%.2fs", result.duration)
20
+
21
+ - if result.failed?
22
+ .analysis-error-details
23
+ %strong Error:
24
+ = result.error
25
+ - if result.error_backtrace&.any?
26
+ %details
27
+ %summary Show backtrace
28
+ %pre.code= result.error_backtrace.join("\n")
29
+
30
+ - if result.sections.any?
31
+ .analysis-output
32
+ -# Group sections by top-level headings (level 0)
33
+ - groups = []
34
+ - current_group = { title: nil, sections: [] }
35
+ - result.sections.each do |section|
36
+ - if section[:type] == :heading && section[:level] == 0
37
+ - groups << current_group if current_group[:title] || current_group[:sections].any?
38
+ - current_group = { title: section[:text], sections: [] }
39
+ - else
40
+ - current_group[:sections] << section
41
+ - groups << current_group if current_group[:title] || current_group[:sections].any?
42
+
43
+ - if groups.size <= 1 && groups.first && groups.first[:title].nil?
44
+ -# No top-level headings, render sections directly without accordions
45
+ - groups.first[:sections].each do |section|
46
+ != render_analysis_section(section)
47
+ - else
48
+ -# Render as accordions (Pico CSS style)
49
+ - titled_groups = groups.select { |g| g[:title] }
50
+ - untitled_group = groups.find { |g| g[:title].nil? }
51
+
52
+ -# Render any sections before first heading without accordion
53
+ - if untitled_group && untitled_group[:sections].any?
54
+ - untitled_group[:sections].each do |section|
55
+ != render_analysis_section(section)
56
+
57
+ -# Render titled groups as accordions with hr separators
58
+ - titled_groups.each_with_index do |group, idx|
59
+ - if idx > 0
60
+ %hr
61
+ %details{name: accordion_name, open: (true if idx == 0)}
62
+ %summary= group[:title]
63
+ - group[:sections].each do |section|
64
+ != render_analysis_section(section)
@@ -20,10 +20,14 @@
20
20
  - if instance.annotations['generated/script']
21
21
  .generated-badge
22
22
  %span.generated-script
23
- Generated by
24
- = instance.annotations['generated/script']
23
+ by
24
+ - script_name = instance.annotations['generated/script']
25
+ %a{href: "/kinds/Import/instances/#{script_name}"}
26
+ = script_name
25
27
  - if instance.annotations['generated/at']
26
- %span.generated-time= time_ago(instance.annotations['generated/at'])
28
+ %span.generated-time
29
+ generated
30
+ = time_ago(instance.annotations['generated/at'])
27
31
  - if instance.has_relations?
28
32
  .graph-container
29
33
  #graphviz.canvas