Skip to content

Instantly share code, notes, and snippets.

@kgilpin
Created January 19, 2023 17:29
Show Gist options
  • Save kgilpin/6f8b30810bb1f53dccb47f2874115756 to your computer and use it in GitHub Desktop.
Save kgilpin/6f8b30810bb1f53dccb47f2874115756 to your computer and use it in GitHub Desktop.

spec/system/application_spec.rb

diff --git a/app/controllers/applications_controller.rb b/app/controllers/applications_controller.rb
index 62d85043..e476ed52 100644
--- a/app/controllers/applications_controller.rb
+++ b/app/controllers/applications_controller.rb
@@ -13,37 +13,37 @@ class ApplicationsController < ApplicationController
   SCANS_PER_PAGE = 25
   private_constant :SCANS_PER_PAGE
 
-  TEST_APP_NAME = 'Featured App'
+  TEST_APP_NAME = "Featured App"
   public_constant :TEST_APP_NAME
 
   FEATURED_APPS = {
     # NOTE: This won't result in AppLand being displayed on the Explore page unless its public flag is set to true.
     # It's useful to have this for development, because appmaps of the other featured apps are not as easily available.
-    'appland/AppLand' => {
-      description: 'AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works'
+    "appland/AppLand" => {
+      description: "AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works",
     },
     # For testing
-    'Featured App' => {
-      description: 'This app is particularly interesting'
+    "Featured App" => {
+      description: "This app is particularly interesting",
     },
-    'land-of-apps/rails_sample_app_6th_ed' => {
-      description: 'Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl'
+    "land-of-apps/rails_sample_app_6th_ed" => {
+      description: "Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl",
     },
-    'land-of-apps/Conjur' => {
-      description: 'Conjur provides secrets management and application identity for modern infrastructure'
+    "land-of-apps/Conjur" => {
+      description: "Conjur provides secrets management and application identity for modern infrastructure",
     },
-    'land-of-apps/discourse' => {
-      description: 'Discourse is the 100% open source discussion platform built for the next decade of the Internet'
+    "land-of-apps/discourse" => {
+      description: "Discourse is the 100% open source discussion platform built for the next decade of the Internet",
     },
-    'land-of-apps/ifme' => {
-      description: 'Free, open source mental health communication web app to share experiences with loved ones'
+    "land-of-apps/ifme" => {
+      description: "Free, open source mental health communication web app to share experiences with loved ones",
     },
-    'land-of-apps/refinerycms' => {
-      description: 'An open source content management system for Rails'
+    "land-of-apps/refinerycms" => {
+      description: "An open source content management system for Rails",
+    },
+    "land-of-apps/spring-petclinic" => {
+      description: "The canonical sample application for Spring Boot",
     },
-    'land-of-apps/spring-petclinic' => {
-      description: 'The canonical sample application for Spring Boot'
-    }
   }.freeze
   private_constant :FEATURED_APPS
 
@@ -52,6 +52,7 @@ class ApplicationsController < ApplicationController
   before_action :find_app, only: %i[show component_diagram related_scenarios scenarios_list update mapset delete_form destroy settings update_repository]
   before_action :check_to_redirect, only: %i[show]
   before_action :find_recent_scans, only: %i[show]
+  before_action :find_preferred_mapsets, only: %i[show]
   before_action :find_summary_stats, only: %i[show]
   before_action :find_mapset, only: %i[component_diagram mapset related_scenarios scenarios_list]
 
@@ -64,9 +65,9 @@ class ApplicationsController < ApplicationController
   end
 
   def process_scenarios
-    render 'processing', locals: {
-      paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true }
-    }
+    render "processing", locals: {
+                           paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true },
+                         }
   end
 
   def update
@@ -79,19 +80,19 @@ class ApplicationsController < ApplicationController
   end
 
   def destroy
-    if(@app.name == params[:application_name])
+    if (@app.name == params[:application_name])
       @app.destroy!(current_user)
-      render partial: 'deleted'
+      render partial: "deleted"
     else
-      @app.errors.add :name, 'Application name does not match'
+      @app.errors.add :name, "Application name does not match"
       @errors = @app.errors
 
-      render 'delete_form', layout: !request.xhr?
+      render "delete_form", layout: !request.xhr?
     end
   end
 
   def examples
-    render layout: 'grid_full_public'
+    render layout: "grid_full_public"
   end
 
   def settings
@@ -103,21 +104,21 @@ class ApplicationsController < ApplicationController
 
   def select_layout
     if configuration.show_legacy_teardown?
-      'three_column'
+      "three_column"
     else
-      'grid_filters'
+      "grid_filters"
     end
   end
 
   def key_stats_owner_type
-    'mapset'
+    "mapset"
   end
 
   def public_apps
     @public_apps ||= search_scope(App, scope: Search::SCOPE_PUBLIC).search(
       offset: params[:offset],
       optional_columns: %i[scenario_count],
-      order: Sequel[:apps][:name]
+      order: Sequel[:apps][:name],
     )
   end
 
@@ -148,13 +149,18 @@ class ApplicationsController < ApplicationController
 
   def find_recent_scans
     @current_page = begin
-      Integer(params[:p], 10)
-    rescue ArgumentError
-      1
-    end
+        Integer(params[:p], 10)
+      rescue ArgumentError
+        1
+      end
 
     num_scans = ScannerJob.count_jobs_for_app(@app)
     @total_pages = (num_scans / Float(SCANS_PER_PAGE)).ceil
     @scans = ScannerJob.most_recent_for_app(@app, limit: SCANS_PER_PAGE, offset: (@current_page - 1) * SCANS_PER_PAGE)
   end
+
+  def find_preferred_mapsets
+    @mapsets = @app.mapsets
+    # byebug
+  end
 end
diff --git a/app/models/app/show.rb b/app/models/app/show.rb
index ff290f91..4aef6e71 100644
--- a/app/models/app/show.rb
+++ b/app/models/app/show.rb
@@ -17,7 +17,7 @@ class App
     end
 
     def mapsets
-      @mapsets ||= Mapset.ordered_list(@app)
+      @mapsets ||= Mapset.ordered_list(@app).map { |mapset| mapset.app = self; mapset }
     end
 
     def org
diff --git a/app/models/scanner_job.rb b/app/models/scanner_job.rb
index ecd8e23a..3913a58a 100644
--- a/app/models/scanner_job.rb
+++ b/app/models/scanner_job.rb
@@ -15,7 +15,11 @@ class ScannerJob
   ]
   private_constant :LIST_ITEM_COLUMNS
 
-  ListItem = Struct.new(*LIST_ITEM_COLUMNS)
+  ListItem = Struct.new(*LIST_ITEM_COLUMNS) do
+    extend Forwardable
+    include Accessor::ScannerJob
+  end
+
   private_constant :ListItem
 
   SummaryItem = Struct.new(
@@ -41,17 +45,17 @@ class ScannerJob
   def self.subquery_findings_count
     Sequel::Model.db[:check_findings_v]
       .select {
-        [
-          Sequel[:check_findings_v][:scanner_job_id],
-          sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count)
-        ]
-      }
+      [
+        Sequel[:check_findings_v][:scanner_job_id],
+        sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count),
+      ]
+    }
       .group_by(:scanner_job_id)
       .as(:findings)
   end
 
   # Retrieve the most recent scanner results for the given user
-  def self.most_recent_for_user(user, limit=10, offset=0)
+  def self.most_recent_for_user(user, limit = 10, offset = 0)
     most_recent_jobs = DAO::Mapset
       .select(Sequel[:scanner_jobs].*, Sequel[:mapsets][:app_id])
       .inner_join(:scanner_jobs, mapset_id: :id)
@@ -90,7 +94,7 @@ class ScannerJob
         Sequel[:scanner_jobs][:mapset_id],
         Sequel[:scanner_jobs][:created_at],
         Sequel[:mapsets][:branch],
-        Sequel.lit('SUBSTRING(mapsets.commit, 1, 7)').as(:commit),
+        Sequel.lit("SUBSTRING(mapsets.commit, 1, 7)").as(:commit),
         Sequel.lit("COALESCE((scanner_jobs.summary->>'numChecks')::integer, 0) as checks_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'new'))::integer, 0) as new_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'deferred'))::integer, 0) as deferred_count"),
@@ -108,12 +112,12 @@ class ScannerJob
   end
 
   def self.summary_by_category(job_id)
-    appmaps_count = DAO::Scanner::Job[job_id].summary['numAppMaps']
+    appmaps_count = DAO::Scanner::Job[job_id].summary["numAppMaps"]
     Sequel::Model(:scanner_checks)
       .select(
         :impact_category,
-        Sequel.lit('COUNT(DISTINCT scanner_checks.check_id) as rules_count'),
-        Sequel.lit('COUNT(finding_occurrences.*)::integer as findings_count'),
+        Sequel.lit("COUNT(DISTINCT scanner_checks.check_id) as rules_count"),
+        Sequel.lit("COUNT(finding_occurrences.*)::integer as findings_count"),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'new\') as new_count'),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'deferred\') as deferred_count')
       )
@@ -124,15 +128,15 @@ class ScannerJob
       .group_by(:impact_category)
       .order_by(:impact_category)
       .map do |row|
-        SummaryItem.new(
-          row[:impact_category],
-          appmaps_count,
-          row[:rules_count],
-          row[:rules_count] * appmaps_count - row[:findings_count],
-          row[:findings_count],
-          row[:new_count],
-          row[:deferred_count],
-        )
+      SummaryItem.new(
+        row[:impact_category],
+        appmaps_count,
+        row[:rules_count],
+        row[:rules_count] * appmaps_count - row[:findings_count],
+        row[:findings_count],
+        row[:new_count],
+        row[:deferred_count],
+      )
     end
   end
 end
diff --git a/app/views/applications/show.html.haml b/app/views/applications/show.html.haml
index cdd497f5..65cb7f19 100644
--- a/app/views/applications/show.html.haml
+++ b/app/views/applications/show.html.haml
@@ -11,22 +11,22 @@
         %img.icon{ src: '/img/gear.svg' }
 
   %section.scan-history
-    - if @scans.empty?
+    - if @scans.empty? && @mapsets.empty?
       .card{ data: { spec: 'no-data' } }
-        %h3.mb-3 Waiting on your first scan...
+        %h3.mb-3 Waiting on your first upload...
         %p Looking for a way to get started? Visit our documentation:
         %ul
           %li
-            %a{ href: 'https://appland.com/docs/analysis/getting-started.html' } Getting started
+            %a{ href: 'https://appmap.io/docs/recording-methods.html', target: '_blank' } Recording AppMaps
           %li
-            %a{ href: 'https://appland.com/docs/analysis/uploading.html' } Uploading findings
+            %a{ href: 'https://appmap.io/docs/openapi.html', target: '_blank'  } Generating OpenAPI
           %li
-            %a{ href: 'https://appland.com/docs/analysis/integrating-with-ci.html' } Integrating with CI
-    - else
+            %a{ href: 'https://appmap.io/docs/analysis', target: '_blank'  } Runtime Analysis
+    - unless @scans.empty?
       = render partial: 'partials/findings_overview'
       %h3.subhead Scan history
       - @scans.each do |scan|
-        %a.card.app{href: scanner_job_path(scan.id), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
+        %a.card.app{href: scanner_job_path(scan), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
           .stacked-title
             .label Branch
             .title{data: { spec: 'branch' }}= scan.branch.present? ? scan.branch : 'unknown'
@@ -45,7 +45,7 @@
             %li.info.column
               .info--key.label Commit
               .info--value{data: { spec: 'commit' }}
-                = scan.commit.present? ? scan.commit[0..7] : 'unknown'
+                = scan.commit.present? ? scan.commit[0..7] : '&nbsp;'.html_safe
             %li.info.column
               .info--key.label Created
               .info--value{data: { spec: 'time-created' }}
@@ -61,3 +61,28 @@
                 %a.page-link{ href: application_path(@app, p: page_num), data: { spec: "page", page: page_num } }= page_num
             %li.page-item{ class: ('disabled' if @current_page == @total_pages) }
               %a.page-link{ href: application_path(@app, p: @current_page + 1), data: { spec: 'next' } } Next
+
+    - unless @mapsets.empty?
+      %h3.subhead Mapset history
+      - @mapsets.each do |mapset|
+        %a.card.app{href: mapset_path(mapset)}
+          .stacked-title
+            .label Branch
+            .title{data: { spec: 'branch' }}= mapset.branch.present? ? mapset.branch : 'unknown'
+          %ul.metrics
+            %li.info.column
+              .info--key.label Commit
+              .info--value{data: { spec: 'commit' }}
+                = mapset.commit.present? ? mapset.commit[0..7] : '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Environment
+              .info--value{data: { spec: 'environment' }}
+                = mapset.environment || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Version
+              .info--value{data: { spec: 'version' }}
+                = mapset.version || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Created
+              .info--value{data: { spec: 'time-created' }}
+                = "#{time_ago_in_words mapset.created_at} ago"
diff --git a/app/views/partials/_findings_overview.html.haml b/app/views/partials/_findings_overview.html.haml
index f0219a56..1676f341 100644
--- a/app/views/partials/_findings_overview.html.haml
+++ b/app/views/partials/_findings_overview.html.haml
@@ -1,6 +1,6 @@
 %section.findings-overview
   .findings-overview__head
-    Trends
+    Analysis Trends
     %span.findings-overview__periods
       - @time_ranges.each do |days|
         %a.findings-overview__period{ class: ('findings-overview__period--active' if @current_time_range == days), href: "?time_range=#{days}", data: { selected: true, spec: 'time-range-select', days: days} }

4 times: added function call `controllers#find_preferred_mapsets`
4 times:   added function call `models#mapsets`
4 times:     added function call `models.ordered_list`
4 times:       added function call `models#ordered_mapset_dataset`
4 times:       added SQL `SELECT *, "mapsets"."name", "mapsets"."branch", "mapsets"."commit", "mapsets"."environment", "mapsets"."version", (SELECT "login" FROM "users" WHERE ("id" = "user_id")), (SELECT count(*) AS "scenario_count" FROM "scenarios" WHERE ("mapset_id" = "mapsets"."id")) FROM "mapsets_preference_ordered" AS "mapsets" WHERE ("mapsets"."app_id" = 1)`
4 times: added function call `helpers#base_slug`
7 times:   added function call `models#org`
20 times: added function call `helpers#mapset_path`
16 times:   added function call `helpers#base_slug`
13 times:     added function call `models#org`

Application page authenticated and authorized with pagination can be navigated via next and previous controls

spec/system/application_spec.rb

diff --git a/app/controllers/applications_controller.rb b/app/controllers/applications_controller.rb
index 62d85043..e476ed52 100644
--- a/app/controllers/applications_controller.rb
+++ b/app/controllers/applications_controller.rb
@@ -13,37 +13,37 @@ class ApplicationsController < ApplicationController
   SCANS_PER_PAGE = 25
   private_constant :SCANS_PER_PAGE
 
-  TEST_APP_NAME = 'Featured App'
+  TEST_APP_NAME = "Featured App"
   public_constant :TEST_APP_NAME
 
   FEATURED_APPS = {
     # NOTE: This won't result in AppLand being displayed on the Explore page unless its public flag is set to true.
     # It's useful to have this for development, because appmaps of the other featured apps are not as easily available.
-    'appland/AppLand' => {
-      description: 'AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works'
+    "appland/AppLand" => {
+      description: "AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works",
     },
     # For testing
-    'Featured App' => {
-      description: 'This app is particularly interesting'
+    "Featured App" => {
+      description: "This app is particularly interesting",
     },
-    'land-of-apps/rails_sample_app_6th_ed' => {
-      description: 'Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl'
+    "land-of-apps/rails_sample_app_6th_ed" => {
+      description: "Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl",
     },
-    'land-of-apps/Conjur' => {
-      description: 'Conjur provides secrets management and application identity for modern infrastructure'
+    "land-of-apps/Conjur" => {
+      description: "Conjur provides secrets management and application identity for modern infrastructure",
     },
-    'land-of-apps/discourse' => {
-      description: 'Discourse is the 100% open source discussion platform built for the next decade of the Internet'
+    "land-of-apps/discourse" => {
+      description: "Discourse is the 100% open source discussion platform built for the next decade of the Internet",
     },
-    'land-of-apps/ifme' => {
-      description: 'Free, open source mental health communication web app to share experiences with loved ones'
+    "land-of-apps/ifme" => {
+      description: "Free, open source mental health communication web app to share experiences with loved ones",
     },
-    'land-of-apps/refinerycms' => {
-      description: 'An open source content management system for Rails'
+    "land-of-apps/refinerycms" => {
+      description: "An open source content management system for Rails",
+    },
+    "land-of-apps/spring-petclinic" => {
+      description: "The canonical sample application for Spring Boot",
     },
-    'land-of-apps/spring-petclinic' => {
-      description: 'The canonical sample application for Spring Boot'
-    }
   }.freeze
   private_constant :FEATURED_APPS
 
@@ -52,6 +52,7 @@ class ApplicationsController < ApplicationController
   before_action :find_app, only: %i[show component_diagram related_scenarios scenarios_list update mapset delete_form destroy settings update_repository]
   before_action :check_to_redirect, only: %i[show]
   before_action :find_recent_scans, only: %i[show]
+  before_action :find_preferred_mapsets, only: %i[show]
   before_action :find_summary_stats, only: %i[show]
   before_action :find_mapset, only: %i[component_diagram mapset related_scenarios scenarios_list]
 
@@ -64,9 +65,9 @@ class ApplicationsController < ApplicationController
   end
 
   def process_scenarios
-    render 'processing', locals: {
-      paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true }
-    }
+    render "processing", locals: {
+                           paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true },
+                         }
   end
 
   def update
@@ -79,19 +80,19 @@ class ApplicationsController < ApplicationController
   end
 
   def destroy
-    if(@app.name == params[:application_name])
+    if (@app.name == params[:application_name])
       @app.destroy!(current_user)
-      render partial: 'deleted'
+      render partial: "deleted"
     else
-      @app.errors.add :name, 'Application name does not match'
+      @app.errors.add :name, "Application name does not match"
       @errors = @app.errors
 
-      render 'delete_form', layout: !request.xhr?
+      render "delete_form", layout: !request.xhr?
     end
   end
 
   def examples
-    render layout: 'grid_full_public'
+    render layout: "grid_full_public"
   end
 
   def settings
@@ -103,21 +104,21 @@ class ApplicationsController < ApplicationController
 
   def select_layout
     if configuration.show_legacy_teardown?
-      'three_column'
+      "three_column"
     else
-      'grid_filters'
+      "grid_filters"
     end
   end
 
   def key_stats_owner_type
-    'mapset'
+    "mapset"
   end
 
   def public_apps
     @public_apps ||= search_scope(App, scope: Search::SCOPE_PUBLIC).search(
       offset: params[:offset],
       optional_columns: %i[scenario_count],
-      order: Sequel[:apps][:name]
+      order: Sequel[:apps][:name],
     )
   end
 
@@ -148,13 +149,18 @@ class ApplicationsController < ApplicationController
 
   def find_recent_scans
     @current_page = begin
-      Integer(params[:p], 10)
-    rescue ArgumentError
-      1
-    end
+        Integer(params[:p], 10)
+      rescue ArgumentError
+        1
+      end
 
     num_scans = ScannerJob.count_jobs_for_app(@app)
     @total_pages = (num_scans / Float(SCANS_PER_PAGE)).ceil
     @scans = ScannerJob.most_recent_for_app(@app, limit: SCANS_PER_PAGE, offset: (@current_page - 1) * SCANS_PER_PAGE)
   end
+
+  def find_preferred_mapsets
+    @mapsets = @app.mapsets
+    # byebug
+  end
 end
diff --git a/app/models/app/show.rb b/app/models/app/show.rb
index ff290f91..4aef6e71 100644
--- a/app/models/app/show.rb
+++ b/app/models/app/show.rb
@@ -17,7 +17,7 @@ class App
     end
 
     def mapsets
-      @mapsets ||= Mapset.ordered_list(@app)
+      @mapsets ||= Mapset.ordered_list(@app).map { |mapset| mapset.app = self; mapset }
     end
 
     def org
diff --git a/app/models/scanner_job.rb b/app/models/scanner_job.rb
index ecd8e23a..3913a58a 100644
--- a/app/models/scanner_job.rb
+++ b/app/models/scanner_job.rb
@@ -15,7 +15,11 @@ class ScannerJob
   ]
   private_constant :LIST_ITEM_COLUMNS
 
-  ListItem = Struct.new(*LIST_ITEM_COLUMNS)
+  ListItem = Struct.new(*LIST_ITEM_COLUMNS) do
+    extend Forwardable
+    include Accessor::ScannerJob
+  end
+
   private_constant :ListItem
 
   SummaryItem = Struct.new(
@@ -41,17 +45,17 @@ class ScannerJob
   def self.subquery_findings_count
     Sequel::Model.db[:check_findings_v]
       .select {
-        [
-          Sequel[:check_findings_v][:scanner_job_id],
-          sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count)
-        ]
-      }
+      [
+        Sequel[:check_findings_v][:scanner_job_id],
+        sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count),
+      ]
+    }
       .group_by(:scanner_job_id)
       .as(:findings)
   end
 
   # Retrieve the most recent scanner results for the given user
-  def self.most_recent_for_user(user, limit=10, offset=0)
+  def self.most_recent_for_user(user, limit = 10, offset = 0)
     most_recent_jobs = DAO::Mapset
       .select(Sequel[:scanner_jobs].*, Sequel[:mapsets][:app_id])
       .inner_join(:scanner_jobs, mapset_id: :id)
@@ -90,7 +94,7 @@ class ScannerJob
         Sequel[:scanner_jobs][:mapset_id],
         Sequel[:scanner_jobs][:created_at],
         Sequel[:mapsets][:branch],
-        Sequel.lit('SUBSTRING(mapsets.commit, 1, 7)').as(:commit),
+        Sequel.lit("SUBSTRING(mapsets.commit, 1, 7)").as(:commit),
         Sequel.lit("COALESCE((scanner_jobs.summary->>'numChecks')::integer, 0) as checks_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'new'))::integer, 0) as new_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'deferred'))::integer, 0) as deferred_count"),
@@ -108,12 +112,12 @@ class ScannerJob
   end
 
   def self.summary_by_category(job_id)
-    appmaps_count = DAO::Scanner::Job[job_id].summary['numAppMaps']
+    appmaps_count = DAO::Scanner::Job[job_id].summary["numAppMaps"]
     Sequel::Model(:scanner_checks)
       .select(
         :impact_category,
-        Sequel.lit('COUNT(DISTINCT scanner_checks.check_id) as rules_count'),
-        Sequel.lit('COUNT(finding_occurrences.*)::integer as findings_count'),
+        Sequel.lit("COUNT(DISTINCT scanner_checks.check_id) as rules_count"),
+        Sequel.lit("COUNT(finding_occurrences.*)::integer as findings_count"),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'new\') as new_count'),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'deferred\') as deferred_count')
       )
@@ -124,15 +128,15 @@ class ScannerJob
       .group_by(:impact_category)
       .order_by(:impact_category)
       .map do |row|
-        SummaryItem.new(
-          row[:impact_category],
-          appmaps_count,
-          row[:rules_count],
-          row[:rules_count] * appmaps_count - row[:findings_count],
-          row[:findings_count],
-          row[:new_count],
-          row[:deferred_count],
-        )
+      SummaryItem.new(
+        row[:impact_category],
+        appmaps_count,
+        row[:rules_count],
+        row[:rules_count] * appmaps_count - row[:findings_count],
+        row[:findings_count],
+        row[:new_count],
+        row[:deferred_count],
+      )
     end
   end
 end
diff --git a/app/views/applications/show.html.haml b/app/views/applications/show.html.haml
index cdd497f5..65cb7f19 100644
--- a/app/views/applications/show.html.haml
+++ b/app/views/applications/show.html.haml
@@ -11,22 +11,22 @@
         %img.icon{ src: '/img/gear.svg' }
 
   %section.scan-history
-    - if @scans.empty?
+    - if @scans.empty? && @mapsets.empty?
       .card{ data: { spec: 'no-data' } }
-        %h3.mb-3 Waiting on your first scan...
+        %h3.mb-3 Waiting on your first upload...
         %p Looking for a way to get started? Visit our documentation:
         %ul
           %li
-            %a{ href: 'https://appland.com/docs/analysis/getting-started.html' } Getting started
+            %a{ href: 'https://appmap.io/docs/recording-methods.html', target: '_blank' } Recording AppMaps
           %li
-            %a{ href: 'https://appland.com/docs/analysis/uploading.html' } Uploading findings
+            %a{ href: 'https://appmap.io/docs/openapi.html', target: '_blank'  } Generating OpenAPI
           %li
-            %a{ href: 'https://appland.com/docs/analysis/integrating-with-ci.html' } Integrating with CI
-    - else
+            %a{ href: 'https://appmap.io/docs/analysis', target: '_blank'  } Runtime Analysis
+    - unless @scans.empty?
       = render partial: 'partials/findings_overview'
       %h3.subhead Scan history
       - @scans.each do |scan|
-        %a.card.app{href: scanner_job_path(scan.id), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
+        %a.card.app{href: scanner_job_path(scan), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
           .stacked-title
             .label Branch
             .title{data: { spec: 'branch' }}= scan.branch.present? ? scan.branch : 'unknown'
@@ -45,7 +45,7 @@
             %li.info.column
               .info--key.label Commit
               .info--value{data: { spec: 'commit' }}
-                = scan.commit.present? ? scan.commit[0..7] : 'unknown'
+                = scan.commit.present? ? scan.commit[0..7] : '&nbsp;'.html_safe
             %li.info.column
               .info--key.label Created
               .info--value{data: { spec: 'time-created' }}
@@ -61,3 +61,28 @@
                 %a.page-link{ href: application_path(@app, p: page_num), data: { spec: "page", page: page_num } }= page_num
             %li.page-item{ class: ('disabled' if @current_page == @total_pages) }
               %a.page-link{ href: application_path(@app, p: @current_page + 1), data: { spec: 'next' } } Next
+
+    - unless @mapsets.empty?
+      %h3.subhead Mapset history
+      - @mapsets.each do |mapset|
+        %a.card.app{href: mapset_path(mapset)}
+          .stacked-title
+            .label Branch
+            .title{data: { spec: 'branch' }}= mapset.branch.present? ? mapset.branch : 'unknown'
+          %ul.metrics
+            %li.info.column
+              .info--key.label Commit
+              .info--value{data: { spec: 'commit' }}
+                = mapset.commit.present? ? mapset.commit[0..7] : '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Environment
+              .info--value{data: { spec: 'environment' }}
+                = mapset.environment || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Version
+              .info--value{data: { spec: 'version' }}
+                = mapset.version || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Created
+              .info--value{data: { spec: 'time-created' }}
+                = "#{time_ago_in_words mapset.created_at} ago"
diff --git a/app/views/partials/_findings_overview.html.haml b/app/views/partials/_findings_overview.html.haml
index f0219a56..1676f341 100644
--- a/app/views/partials/_findings_overview.html.haml
+++ b/app/views/partials/_findings_overview.html.haml
@@ -1,6 +1,6 @@
 %section.findings-overview
   .findings-overview__head
-    Trends
+    Analysis Trends
     %span.findings-overview__periods
       - @time_ranges.each do |days|
         %a.findings-overview__period{ class: ('findings-overview__period--active' if @current_time_range == days), href: "?time_range=#{days}", data: { selected: true, spec: 'time-range-select', days: days} }

2 times: added function call `controllers#find_preferred_mapsets`
2 times:   added function call `models#mapsets`
2 times:     added function call `models.ordered_list`
2 times:       added function call `models#ordered_mapset_dataset`
2 times:       added SQL `SELECT *, "mapsets"."name", "mapsets"."branch", "mapsets"."commit", "mapsets"."environment", "mapsets"."version", (SELECT "login" FROM "users" WHERE ("id" = "user_id")), (SELECT count(*) AS "scenario_count" FROM "scenarios" WHERE ("mapset_id" = "mapsets"."id")) FROM "mapsets_preference_ordered" AS "mapsets" WHERE ("mapsets"."app_id" = 1)`
2 times: added function call `helpers#base_slug`
4 times:   added function call `models#org`
10 times: added function call `helpers#mapset_path`
8 times:   added function call `helpers#base_slug`
6 times:     added function call `models#org`

Application page authenticated and authorized with pagination renders the expected scans on each page

spec/requests/application_component_diagram_spec.rb

diff --git a/app/models/app/show.rb b/app/models/app/show.rb
index ff290f91..4aef6e71 100644
--- a/app/models/app/show.rb
+++ b/app/models/app/show.rb
@@ -17,7 +17,7 @@ class App
     end
 
     def mapsets
-      @mapsets ||= Mapset.ordered_list(@app)
+      @mapsets ||= Mapset.ordered_list(@app).map { |mapset| mapset.app = self; mapset }
     end
 
     def org
diff --git a/app/models/mapset.rb b/app/models/mapset.rb
index 68c6d9b6..8d8782a2 100644
--- a/app/models/mapset.rb
+++ b/app/models/mapset.rb
@@ -13,12 +13,13 @@ class Mapset
     def find_in_app!(app, id)
       mapset = app.mapsets.find { |mapset| mapset.id == id }
 
-      raise Exceptions::RecordNotFound.new('Mapset', id) unless mapset
+      raise Exceptions::RecordNotFound.new("Mapset", id) unless mapset
 
       Mapset::Show.new(DAO::Mapset[mapset.id])
     end
 
     # ordered_list provides a list of Mapsets in app-preferred order.
+    # To access this data, use Mapset::Show#mapsets, rather than calling this method directly.
     def ordered_list(app)
       sum_scenario_count = 0
       app.ordered_mapset_dataset
@@ -34,13 +35,13 @@ class Mapset
             .select(Sequel.as(Sequel.function(:count).*, :scenario_count))
         )
         .map do |row|
-          Mapset::ListItem.new(*row.values.values_at(:id, :created_at, :name, :branch,
-            :commit, :environment, :version, :login)).tap do |mapset|
-              sum_scenario_count += row[:scenario_count]
-              mapset.scenario_count = row[:scenario_count]
+        Mapset::ListItem.new(*row.values.values_at(:id, :created_at, :name, :branch,
+                                                   :commit, :environment, :version, :login)).tap do |mapset|
+          sum_scenario_count += row[:scenario_count]
+          mapset.scenario_count = row[:scenario_count]
         end
       end
-      .filter do |mapset|
+        .filter do |mapset|
         mapset.scenario_count > 0 || sum_scenario_count == 0
       end
     end


ApplicationsController .component_diagram logged in for a single feature gets a component diagram of matching scenarios

spec/system/dashboard_summary_spec.rb

diff --git a/app/controllers/applications_controller.rb b/app/controllers/applications_controller.rb
index 62d85043..e476ed52 100644
--- a/app/controllers/applications_controller.rb
+++ b/app/controllers/applications_controller.rb
@@ -13,37 +13,37 @@ class ApplicationsController < ApplicationController
   SCANS_PER_PAGE = 25
   private_constant :SCANS_PER_PAGE
 
-  TEST_APP_NAME = 'Featured App'
+  TEST_APP_NAME = "Featured App"
   public_constant :TEST_APP_NAME
 
   FEATURED_APPS = {
     # NOTE: This won't result in AppLand being displayed on the Explore page unless its public flag is set to true.
     # It's useful to have this for development, because appmaps of the other featured apps are not as easily available.
-    'appland/AppLand' => {
-      description: 'AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works'
+    "appland/AppLand" => {
+      description: "AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works",
     },
     # For testing
-    'Featured App' => {
-      description: 'This app is particularly interesting'
+    "Featured App" => {
+      description: "This app is particularly interesting",
     },
-    'land-of-apps/rails_sample_app_6th_ed' => {
-      description: 'Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl'
+    "land-of-apps/rails_sample_app_6th_ed" => {
+      description: "Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl",
     },
-    'land-of-apps/Conjur' => {
-      description: 'Conjur provides secrets management and application identity for modern infrastructure'
+    "land-of-apps/Conjur" => {
+      description: "Conjur provides secrets management and application identity for modern infrastructure",
     },
-    'land-of-apps/discourse' => {
-      description: 'Discourse is the 100% open source discussion platform built for the next decade of the Internet'
+    "land-of-apps/discourse" => {
+      description: "Discourse is the 100% open source discussion platform built for the next decade of the Internet",
     },
-    'land-of-apps/ifme' => {
-      description: 'Free, open source mental health communication web app to share experiences with loved ones'
+    "land-of-apps/ifme" => {
+      description: "Free, open source mental health communication web app to share experiences with loved ones",
     },
-    'land-of-apps/refinerycms' => {
-      description: 'An open source content management system for Rails'
+    "land-of-apps/refinerycms" => {
+      description: "An open source content management system for Rails",
+    },
+    "land-of-apps/spring-petclinic" => {
+      description: "The canonical sample application for Spring Boot",
     },
-    'land-of-apps/spring-petclinic' => {
-      description: 'The canonical sample application for Spring Boot'
-    }
   }.freeze
   private_constant :FEATURED_APPS
 
@@ -52,6 +52,7 @@ class ApplicationsController < ApplicationController
   before_action :find_app, only: %i[show component_diagram related_scenarios scenarios_list update mapset delete_form destroy settings update_repository]
   before_action :check_to_redirect, only: %i[show]
   before_action :find_recent_scans, only: %i[show]
+  before_action :find_preferred_mapsets, only: %i[show]
   before_action :find_summary_stats, only: %i[show]
   before_action :find_mapset, only: %i[component_diagram mapset related_scenarios scenarios_list]
 
@@ -64,9 +65,9 @@ class ApplicationsController < ApplicationController
   end
 
   def process_scenarios
-    render 'processing', locals: {
-      paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true }
-    }
+    render "processing", locals: {
+                           paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true },
+                         }
   end
 
   def update
@@ -79,19 +80,19 @@ class ApplicationsController < ApplicationController
   end
 
   def destroy
-    if(@app.name == params[:application_name])
+    if (@app.name == params[:application_name])
       @app.destroy!(current_user)
-      render partial: 'deleted'
+      render partial: "deleted"
     else
-      @app.errors.add :name, 'Application name does not match'
+      @app.errors.add :name, "Application name does not match"
       @errors = @app.errors
 
-      render 'delete_form', layout: !request.xhr?
+      render "delete_form", layout: !request.xhr?
     end
   end
 
   def examples
-    render layout: 'grid_full_public'
+    render layout: "grid_full_public"
   end
 
   def settings
@@ -103,21 +104,21 @@ class ApplicationsController < ApplicationController
 
   def select_layout
     if configuration.show_legacy_teardown?
-      'three_column'
+      "three_column"
     else
-      'grid_filters'
+      "grid_filters"
     end
   end
 
   def key_stats_owner_type
-    'mapset'
+    "mapset"
   end
 
   def public_apps
     @public_apps ||= search_scope(App, scope: Search::SCOPE_PUBLIC).search(
       offset: params[:offset],
       optional_columns: %i[scenario_count],
-      order: Sequel[:apps][:name]
+      order: Sequel[:apps][:name],
     )
   end
 
@@ -148,13 +149,18 @@ class ApplicationsController < ApplicationController
 
   def find_recent_scans
     @current_page = begin
-      Integer(params[:p], 10)
-    rescue ArgumentError
-      1
-    end
+        Integer(params[:p], 10)
+      rescue ArgumentError
+        1
+      end
 
     num_scans = ScannerJob.count_jobs_for_app(@app)
     @total_pages = (num_scans / Float(SCANS_PER_PAGE)).ceil
     @scans = ScannerJob.most_recent_for_app(@app, limit: SCANS_PER_PAGE, offset: (@current_page - 1) * SCANS_PER_PAGE)
   end
+
+  def find_preferred_mapsets
+    @mapsets = @app.mapsets
+    # byebug
+  end
 end
diff --git a/app/models/app/show.rb b/app/models/app/show.rb
index ff290f91..4aef6e71 100644
--- a/app/models/app/show.rb
+++ b/app/models/app/show.rb
@@ -17,7 +17,7 @@ class App
     end
 
     def mapsets
-      @mapsets ||= Mapset.ordered_list(@app)
+      @mapsets ||= Mapset.ordered_list(@app).map { |mapset| mapset.app = self; mapset }
     end
 
     def org
diff --git a/app/models/scanner_job.rb b/app/models/scanner_job.rb
index ecd8e23a..3913a58a 100644
--- a/app/models/scanner_job.rb
+++ b/app/models/scanner_job.rb
@@ -15,7 +15,11 @@ class ScannerJob
   ]
   private_constant :LIST_ITEM_COLUMNS
 
-  ListItem = Struct.new(*LIST_ITEM_COLUMNS)
+  ListItem = Struct.new(*LIST_ITEM_COLUMNS) do
+    extend Forwardable
+    include Accessor::ScannerJob
+  end
+
   private_constant :ListItem
 
   SummaryItem = Struct.new(
@@ -41,17 +45,17 @@ class ScannerJob
   def self.subquery_findings_count
     Sequel::Model.db[:check_findings_v]
       .select {
-        [
-          Sequel[:check_findings_v][:scanner_job_id],
-          sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count)
-        ]
-      }
+      [
+        Sequel[:check_findings_v][:scanner_job_id],
+        sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count),
+      ]
+    }
       .group_by(:scanner_job_id)
       .as(:findings)
   end
 
   # Retrieve the most recent scanner results for the given user
-  def self.most_recent_for_user(user, limit=10, offset=0)
+  def self.most_recent_for_user(user, limit = 10, offset = 0)
     most_recent_jobs = DAO::Mapset
       .select(Sequel[:scanner_jobs].*, Sequel[:mapsets][:app_id])
       .inner_join(:scanner_jobs, mapset_id: :id)
@@ -90,7 +94,7 @@ class ScannerJob
         Sequel[:scanner_jobs][:mapset_id],
         Sequel[:scanner_jobs][:created_at],
         Sequel[:mapsets][:branch],
-        Sequel.lit('SUBSTRING(mapsets.commit, 1, 7)').as(:commit),
+        Sequel.lit("SUBSTRING(mapsets.commit, 1, 7)").as(:commit),
         Sequel.lit("COALESCE((scanner_jobs.summary->>'numChecks')::integer, 0) as checks_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'new'))::integer, 0) as new_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'deferred'))::integer, 0) as deferred_count"),
@@ -108,12 +112,12 @@ class ScannerJob
   end
 
   def self.summary_by_category(job_id)
-    appmaps_count = DAO::Scanner::Job[job_id].summary['numAppMaps']
+    appmaps_count = DAO::Scanner::Job[job_id].summary["numAppMaps"]
     Sequel::Model(:scanner_checks)
       .select(
         :impact_category,
-        Sequel.lit('COUNT(DISTINCT scanner_checks.check_id) as rules_count'),
-        Sequel.lit('COUNT(finding_occurrences.*)::integer as findings_count'),
+        Sequel.lit("COUNT(DISTINCT scanner_checks.check_id) as rules_count"),
+        Sequel.lit("COUNT(finding_occurrences.*)::integer as findings_count"),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'new\') as new_count'),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'deferred\') as deferred_count')
       )
@@ -124,15 +128,15 @@ class ScannerJob
       .group_by(:impact_category)
       .order_by(:impact_category)
       .map do |row|
-        SummaryItem.new(
-          row[:impact_category],
-          appmaps_count,
-          row[:rules_count],
-          row[:rules_count] * appmaps_count - row[:findings_count],
-          row[:findings_count],
-          row[:new_count],
-          row[:deferred_count],
-        )
+      SummaryItem.new(
+        row[:impact_category],
+        appmaps_count,
+        row[:rules_count],
+        row[:rules_count] * appmaps_count - row[:findings_count],
+        row[:findings_count],
+        row[:new_count],
+        row[:deferred_count],
+      )
     end
   end
 end
diff --git a/app/views/applications/show.html.haml b/app/views/applications/show.html.haml
index cdd497f5..65cb7f19 100644
--- a/app/views/applications/show.html.haml
+++ b/app/views/applications/show.html.haml
@@ -11,22 +11,22 @@
         %img.icon{ src: '/img/gear.svg' }
 
   %section.scan-history
-    - if @scans.empty?
+    - if @scans.empty? && @mapsets.empty?
       .card{ data: { spec: 'no-data' } }
-        %h3.mb-3 Waiting on your first scan...
+        %h3.mb-3 Waiting on your first upload...
         %p Looking for a way to get started? Visit our documentation:
         %ul
           %li
-            %a{ href: 'https://appland.com/docs/analysis/getting-started.html' } Getting started
+            %a{ href: 'https://appmap.io/docs/recording-methods.html', target: '_blank' } Recording AppMaps
           %li
-            %a{ href: 'https://appland.com/docs/analysis/uploading.html' } Uploading findings
+            %a{ href: 'https://appmap.io/docs/openapi.html', target: '_blank'  } Generating OpenAPI
           %li
-            %a{ href: 'https://appland.com/docs/analysis/integrating-with-ci.html' } Integrating with CI
-    - else
+            %a{ href: 'https://appmap.io/docs/analysis', target: '_blank'  } Runtime Analysis
+    - unless @scans.empty?
       = render partial: 'partials/findings_overview'
       %h3.subhead Scan history
       - @scans.each do |scan|
-        %a.card.app{href: scanner_job_path(scan.id), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
+        %a.card.app{href: scanner_job_path(scan), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
           .stacked-title
             .label Branch
             .title{data: { spec: 'branch' }}= scan.branch.present? ? scan.branch : 'unknown'
@@ -45,7 +45,7 @@
             %li.info.column
               .info--key.label Commit
               .info--value{data: { spec: 'commit' }}
-                = scan.commit.present? ? scan.commit[0..7] : 'unknown'
+                = scan.commit.present? ? scan.commit[0..7] : '&nbsp;'.html_safe
             %li.info.column
               .info--key.label Created
               .info--value{data: { spec: 'time-created' }}
@@ -61,3 +61,28 @@
                 %a.page-link{ href: application_path(@app, p: page_num), data: { spec: "page", page: page_num } }= page_num
             %li.page-item{ class: ('disabled' if @current_page == @total_pages) }
               %a.page-link{ href: application_path(@app, p: @current_page + 1), data: { spec: 'next' } } Next
+
+    - unless @mapsets.empty?
+      %h3.subhead Mapset history
+      - @mapsets.each do |mapset|
+        %a.card.app{href: mapset_path(mapset)}
+          .stacked-title
+            .label Branch
+            .title{data: { spec: 'branch' }}= mapset.branch.present? ? mapset.branch : 'unknown'
+          %ul.metrics
+            %li.info.column
+              .info--key.label Commit
+              .info--value{data: { spec: 'commit' }}
+                = mapset.commit.present? ? mapset.commit[0..7] : '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Environment
+              .info--value{data: { spec: 'environment' }}
+                = mapset.environment || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Version
+              .info--value{data: { spec: 'version' }}
+                = mapset.version || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Created
+              .info--value{data: { spec: 'time-created' }}
+                = "#{time_ago_in_words mapset.created_at} ago"
diff --git a/app/views/partials/_findings_overview.html.haml b/app/views/partials/_findings_overview.html.haml
index f0219a56..1676f341 100644
--- a/app/views/partials/_findings_overview.html.haml
+++ b/app/views/partials/_findings_overview.html.haml
@@ -1,6 +1,6 @@
 %section.findings-overview
   .findings-overview__head
-    Trends
+    Analysis Trends
     %span.findings-overview__periods
       - @time_ranges.each do |days|
         %a.findings-overview__period{ class: ('findings-overview__period--active' if @current_time_range == days), href: "?time_range=#{days}", data: { selected: true, spec: 'time-range-select', days: days} }

added function call `controllers#find_preferred_mapsets`
  added function call `models#mapsets`
    added function call `models.ordered_list`
      added function call `models#ordered_mapset_dataset`
      added SQL `SELECT *, "mapsets"."name", "mapsets"."branch", "mapsets"."commit", "mapsets"."environment", "mapsets"."version", (SELECT "login" FROM "users" WHERE ("id" = "user_id")), (SELECT count(*) AS "scenario_count" FROM "scenarios" WHERE ("mapset_id" = "mapsets"."id")) FROM "mapsets_preference_ordered" AS "mapsets" WHERE ("mapsets"."app_id" = 1)`
added function call `helpers#mapset_path`
  added function call `helpers#base_slug`
    added function call `models#org`

Dashboard summary with no data allows the user to select a new time range

spec/system/early_access_spec.rb

diff --git a/app/controllers/applications_controller.rb b/app/controllers/applications_controller.rb
index 62d85043..e476ed52 100644
--- a/app/controllers/applications_controller.rb
+++ b/app/controllers/applications_controller.rb
@@ -13,37 +13,37 @@ class ApplicationsController < ApplicationController
   SCANS_PER_PAGE = 25
   private_constant :SCANS_PER_PAGE
 
-  TEST_APP_NAME = 'Featured App'
+  TEST_APP_NAME = "Featured App"
   public_constant :TEST_APP_NAME
 
   FEATURED_APPS = {
     # NOTE: This won't result in AppLand being displayed on the Explore page unless its public flag is set to true.
     # It's useful to have this for development, because appmaps of the other featured apps are not as easily available.
-    'appland/AppLand' => {
-      description: 'AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works'
+    "appland/AppLand" => {
+      description: "AppLand records your running code, and automatically creates visualizations and data tables that show you exactly how your code works",
     },
     # For testing
-    'Featured App' => {
-      description: 'This app is particularly interesting'
+    "Featured App" => {
+      description: "This app is particularly interesting",
     },
-    'land-of-apps/rails_sample_app_6th_ed' => {
-      description: 'Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl'
+    "land-of-apps/rails_sample_app_6th_ed" => {
+      description: "Reference implementation of the sample application from Ruby on Rails Tutorial: Learn Web Development with Rails (6th Edition) by Michael Hartl",
     },
-    'land-of-apps/Conjur' => {
-      description: 'Conjur provides secrets management and application identity for modern infrastructure'
+    "land-of-apps/Conjur" => {
+      description: "Conjur provides secrets management and application identity for modern infrastructure",
     },
-    'land-of-apps/discourse' => {
-      description: 'Discourse is the 100% open source discussion platform built for the next decade of the Internet'
+    "land-of-apps/discourse" => {
+      description: "Discourse is the 100% open source discussion platform built for the next decade of the Internet",
     },
-    'land-of-apps/ifme' => {
-      description: 'Free, open source mental health communication web app to share experiences with loved ones'
+    "land-of-apps/ifme" => {
+      description: "Free, open source mental health communication web app to share experiences with loved ones",
     },
-    'land-of-apps/refinerycms' => {
-      description: 'An open source content management system for Rails'
+    "land-of-apps/refinerycms" => {
+      description: "An open source content management system for Rails",
+    },
+    "land-of-apps/spring-petclinic" => {
+      description: "The canonical sample application for Spring Boot",
     },
-    'land-of-apps/spring-petclinic' => {
-      description: 'The canonical sample application for Spring Boot'
-    }
   }.freeze
   private_constant :FEATURED_APPS
 
@@ -52,6 +52,7 @@ class ApplicationsController < ApplicationController
   before_action :find_app, only: %i[show component_diagram related_scenarios scenarios_list update mapset delete_form destroy settings update_repository]
   before_action :check_to_redirect, only: %i[show]
   before_action :find_recent_scans, only: %i[show]
+  before_action :find_preferred_mapsets, only: %i[show]
   before_action :find_summary_stats, only: %i[show]
   before_action :find_mapset, only: %i[component_diagram mapset related_scenarios scenarios_list]
 
@@ -64,9 +65,9 @@ class ApplicationsController < ApplicationController
   end
 
   def process_scenarios
-    render 'processing', locals: {
-      paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true }
-    }
+    render "processing", locals: {
+                           paths: @mapset.pending_scenarios.select(:uuid).map { |s| scenario_path s, process: true },
+                         }
   end
 
   def update
@@ -79,19 +80,19 @@ class ApplicationsController < ApplicationController
   end
 
   def destroy
-    if(@app.name == params[:application_name])
+    if (@app.name == params[:application_name])
       @app.destroy!(current_user)
-      render partial: 'deleted'
+      render partial: "deleted"
     else
-      @app.errors.add :name, 'Application name does not match'
+      @app.errors.add :name, "Application name does not match"
       @errors = @app.errors
 
-      render 'delete_form', layout: !request.xhr?
+      render "delete_form", layout: !request.xhr?
     end
   end
 
   def examples
-    render layout: 'grid_full_public'
+    render layout: "grid_full_public"
   end
 
   def settings
@@ -103,21 +104,21 @@ class ApplicationsController < ApplicationController
 
   def select_layout
     if configuration.show_legacy_teardown?
-      'three_column'
+      "three_column"
     else
-      'grid_filters'
+      "grid_filters"
     end
   end
 
   def key_stats_owner_type
-    'mapset'
+    "mapset"
   end
 
   def public_apps
     @public_apps ||= search_scope(App, scope: Search::SCOPE_PUBLIC).search(
       offset: params[:offset],
       optional_columns: %i[scenario_count],
-      order: Sequel[:apps][:name]
+      order: Sequel[:apps][:name],
     )
   end
 
@@ -148,13 +149,18 @@ class ApplicationsController < ApplicationController
 
   def find_recent_scans
     @current_page = begin
-      Integer(params[:p], 10)
-    rescue ArgumentError
-      1
-    end
+        Integer(params[:p], 10)
+      rescue ArgumentError
+        1
+      end
 
     num_scans = ScannerJob.count_jobs_for_app(@app)
     @total_pages = (num_scans / Float(SCANS_PER_PAGE)).ceil
     @scans = ScannerJob.most_recent_for_app(@app, limit: SCANS_PER_PAGE, offset: (@current_page - 1) * SCANS_PER_PAGE)
   end
+
+  def find_preferred_mapsets
+    @mapsets = @app.mapsets
+    # byebug
+  end
 end
diff --git a/app/models/app/show.rb b/app/models/app/show.rb
index ff290f91..4aef6e71 100644
--- a/app/models/app/show.rb
+++ b/app/models/app/show.rb
@@ -17,7 +17,7 @@ class App
     end
 
     def mapsets
-      @mapsets ||= Mapset.ordered_list(@app)
+      @mapsets ||= Mapset.ordered_list(@app).map { |mapset| mapset.app = self; mapset }
     end
 
     def org
diff --git a/app/models/scanner_job.rb b/app/models/scanner_job.rb
index ecd8e23a..3913a58a 100644
--- a/app/models/scanner_job.rb
+++ b/app/models/scanner_job.rb
@@ -15,7 +15,11 @@ class ScannerJob
   ]
   private_constant :LIST_ITEM_COLUMNS
 
-  ListItem = Struct.new(*LIST_ITEM_COLUMNS)
+  ListItem = Struct.new(*LIST_ITEM_COLUMNS) do
+    extend Forwardable
+    include Accessor::ScannerJob
+  end
+
   private_constant :ListItem
 
   SummaryItem = Struct.new(
@@ -41,17 +45,17 @@ class ScannerJob
   def self.subquery_findings_count
     Sequel::Model.db[:check_findings_v]
       .select {
-        [
-          Sequel[:check_findings_v][:scanner_job_id],
-          sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count)
-        ]
-      }
+      [
+        Sequel[:check_findings_v][:scanner_job_id],
+        sum(Sequel[:check_findings_v][:findings_count]).cast(:integer).as(:findings_count),
+      ]
+    }
       .group_by(:scanner_job_id)
       .as(:findings)
   end
 
   # Retrieve the most recent scanner results for the given user
-  def self.most_recent_for_user(user, limit=10, offset=0)
+  def self.most_recent_for_user(user, limit = 10, offset = 0)
     most_recent_jobs = DAO::Mapset
       .select(Sequel[:scanner_jobs].*, Sequel[:mapsets][:app_id])
       .inner_join(:scanner_jobs, mapset_id: :id)
@@ -90,7 +94,7 @@ class ScannerJob
         Sequel[:scanner_jobs][:mapset_id],
         Sequel[:scanner_jobs][:created_at],
         Sequel[:mapsets][:branch],
-        Sequel.lit('SUBSTRING(mapsets.commit, 1, 7)').as(:commit),
+        Sequel.lit("SUBSTRING(mapsets.commit, 1, 7)").as(:commit),
         Sequel.lit("COALESCE((scanner_jobs.summary->>'numChecks')::integer, 0) as checks_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'new'))::integer, 0) as new_count"),
         Sequel.lit("COALESCE((SUM(check_findings_v.findings_count) FILTER (WHERE check_findings_v.status = 'deferred'))::integer, 0) as deferred_count"),
@@ -108,12 +112,12 @@ class ScannerJob
   end
 
   def self.summary_by_category(job_id)
-    appmaps_count = DAO::Scanner::Job[job_id].summary['numAppMaps']
+    appmaps_count = DAO::Scanner::Job[job_id].summary["numAppMaps"]
     Sequel::Model(:scanner_checks)
       .select(
         :impact_category,
-        Sequel.lit('COUNT(DISTINCT scanner_checks.check_id) as rules_count'),
-        Sequel.lit('COUNT(finding_occurrences.*)::integer as findings_count'),
+        Sequel.lit("COUNT(DISTINCT scanner_checks.check_id) as rules_count"),
+        Sequel.lit("COUNT(finding_occurrences.*)::integer as findings_count"),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'new\') as new_count'),
         Sequel.lit('COUNT(finding_occurrences.*) FILTER (WHERE status = \'deferred\') as deferred_count')
       )
@@ -124,15 +128,15 @@ class ScannerJob
       .group_by(:impact_category)
       .order_by(:impact_category)
       .map do |row|
-        SummaryItem.new(
-          row[:impact_category],
-          appmaps_count,
-          row[:rules_count],
-          row[:rules_count] * appmaps_count - row[:findings_count],
-          row[:findings_count],
-          row[:new_count],
-          row[:deferred_count],
-        )
+      SummaryItem.new(
+        row[:impact_category],
+        appmaps_count,
+        row[:rules_count],
+        row[:rules_count] * appmaps_count - row[:findings_count],
+        row[:findings_count],
+        row[:new_count],
+        row[:deferred_count],
+      )
     end
   end
 end
diff --git a/app/views/applications/show.html.haml b/app/views/applications/show.html.haml
index cdd497f5..65cb7f19 100644
--- a/app/views/applications/show.html.haml
+++ b/app/views/applications/show.html.haml
@@ -11,22 +11,22 @@
         %img.icon{ src: '/img/gear.svg' }
 
   %section.scan-history
-    - if @scans.empty?
+    - if @scans.empty? && @mapsets.empty?
       .card{ data: { spec: 'no-data' } }
-        %h3.mb-3 Waiting on your first scan...
+        %h3.mb-3 Waiting on your first upload...
         %p Looking for a way to get started? Visit our documentation:
         %ul
           %li
-            %a{ href: 'https://appland.com/docs/analysis/getting-started.html' } Getting started
+            %a{ href: 'https://appmap.io/docs/recording-methods.html', target: '_blank' } Recording AppMaps
           %li
-            %a{ href: 'https://appland.com/docs/analysis/uploading.html' } Uploading findings
+            %a{ href: 'https://appmap.io/docs/openapi.html', target: '_blank'  } Generating OpenAPI
           %li
-            %a{ href: 'https://appland.com/docs/analysis/integrating-with-ci.html' } Integrating with CI
-    - else
+            %a{ href: 'https://appmap.io/docs/analysis', target: '_blank'  } Runtime Analysis
+    - unless @scans.empty?
       = render partial: 'partials/findings_overview'
       %h3.subhead Scan history
       - @scans.each do |scan|
-        %a.card.app{href: scanner_job_path(scan.id), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
+        %a.card.app{href: scanner_job_path(scan), data: { scan_status: scan.new_count == 0 ? 'pass' : 'fail', spec: 'scan' }}
           .stacked-title
             .label Branch
             .title{data: { spec: 'branch' }}= scan.branch.present? ? scan.branch : 'unknown'
@@ -45,7 +45,7 @@
             %li.info.column
               .info--key.label Commit
               .info--value{data: { spec: 'commit' }}
-                = scan.commit.present? ? scan.commit[0..7] : 'unknown'
+                = scan.commit.present? ? scan.commit[0..7] : '&nbsp;'.html_safe
             %li.info.column
               .info--key.label Created
               .info--value{data: { spec: 'time-created' }}
@@ -61,3 +61,28 @@
                 %a.page-link{ href: application_path(@app, p: page_num), data: { spec: "page", page: page_num } }= page_num
             %li.page-item{ class: ('disabled' if @current_page == @total_pages) }
               %a.page-link{ href: application_path(@app, p: @current_page + 1), data: { spec: 'next' } } Next
+
+    - unless @mapsets.empty?
+      %h3.subhead Mapset history
+      - @mapsets.each do |mapset|
+        %a.card.app{href: mapset_path(mapset)}
+          .stacked-title
+            .label Branch
+            .title{data: { spec: 'branch' }}= mapset.branch.present? ? mapset.branch : 'unknown'
+          %ul.metrics
+            %li.info.column
+              .info--key.label Commit
+              .info--value{data: { spec: 'commit' }}
+                = mapset.commit.present? ? mapset.commit[0..7] : '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Environment
+              .info--value{data: { spec: 'environment' }}
+                = mapset.environment || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Version
+              .info--value{data: { spec: 'version' }}
+                = mapset.version || '&nbsp;'.html_safe
+            %li.info.column
+              .info--key.label Created
+              .info--value{data: { spec: 'time-created' }}
+                = "#{time_ago_in_words mapset.created_at} ago"
diff --git a/app/views/partials/_findings_overview.html.haml b/app/views/partials/_findings_overview.html.haml
index f0219a56..1676f341 100644
--- a/app/views/partials/_findings_overview.html.haml
+++ b/app/views/partials/_findings_overview.html.haml
@@ -1,6 +1,6 @@
 %section.findings-overview
   .findings-overview__head
-    Trends
+    Analysis Trends
     %span.findings-overview__periods
       - @time_ranges.each do |days|
         %a.findings-overview__period{ class: ('findings-overview__period--active' if @current_time_range == days), href: "?time_range=#{days}", data: { selected: true, spec: 'time-range-select', days: days} }
diff --git a/app/views/scanner_jobs/_meta.html.haml b/app/views/scanner_jobs/_meta.html.haml
index dfb4a6e0..eb20cdff 100644
--- a/app/views/scanner_jobs/_meta.html.haml
+++ b/app/views/scanner_jobs/_meta.html.haml
@@ -25,6 +25,11 @@
   .scan-meta__details
     .card.scan-meta__card
       %dl.scan-meta__dl
+        .scan-meta__dl-row
+          %dt.scan-meta__dt
+            Scan ID:
+          %dd.scan-meta__dd
+            = @job.id
         .scan-meta__dl-row
           %dt.scan-meta__dt
             Branch:
@@ -32,11 +37,11 @@
             = @job.branch
         .scan-meta__dl-row
           %dt.scan-meta__dt
-            Last scanned:
+            Created:
           %dd.scan-meta__dd
-            = @job.created_at
+            = "#{time_ago_in_words(@job.created_at)} ago"
         .scan-meta__dl-row
           %dt.scan-meta__dt
-            Scan ID:
+            AppMap data:
           %dd.scan-meta__dd
-            = @job.id
+            = link_to "analysis mapset ##{@job.mapset.id}", mapset_path(@job.mapset)

removed SQL `SELECT "pg_attribute"."attname" AS "name", CAST("pg_attribute"."atttypid" AS integer) AS "oid", CAST("basetype"."oid" AS integer) AS "base_oid", format_type("basetype"."oid", "pg_type"."typtypmod") AS "db_base_type", format_type("pg_type"."oid", "pg_attribute"."atttypmod") AS "db_type", pg_get_expr("pg_attrdef"."adbin", "pg_class"."oid") AS "default", NOT "pg_attribute"."attnotnull" AS "allow_null", COALESCE(("pg_attribute"."attnum" = ANY("pg_index"."indkey")), false) AS "primary_key", "pg_attribute"."attidentity", ("pg_attribute"."attgenerated" != '') AS "generated" FROM "pg_class" INNER JOIN "pg_attribute" ON ("pg_attribute"."attrelid" = "pg_class"."oid") INNER JOIN "pg_type" ON ("pg_type"."oid" = "pg_attribute"."atttypid") LEFT OUTER JOIN "pg_type" AS "basetype" ON ("basetype"."oid" = "pg_type"."typbasetype") LEFT OUTER JOIN "pg_attrdef" ON (("pg_attrdef"."adrelid" = "pg_class"."oid") AND ("pg_attrdef"."adnum" = "pg_attribute"."attnum")) LEFT OUTER JOIN "pg_index" ON (("pg_index"."indrelid" = "pg_class"."oid") AND ("pg_index"."indisprimary" IS TRUE)) WHERE (("pg_attribute"."attisdropped" IS FALSE) AND ("pg_attribute"."attnum" > 0) AND ("pg_class"."oid" = CAST(CAST('"scanner_checks"' AS regclass) AS oid))) ORDER BY "pg_attribute"."attnum"`
2 times: added function call `models#mapset`
2 times:   added function call `models#to_model`
2 times: added function call `helpers#mapset_path`
  added function call `models#app`
    added function call `models#to_model`
2 times:   added function call `helpers#base_slug`
2 times:     added function call `models#org`
      added SQL `SELECT * FROM "orgs" WHERE "id" = 2`
      added function call `models#to_model`
removed SQL `SELECT "pg_attribute"."attname" AS "name", CAST("pg_attribute"."atttypid" AS integer) AS "oid", CAST("basetype"."oid" AS integer) AS "base_oid", format_type("basetype"."oid", "pg_type"."typtypmod") AS "db_base_type", format_type("pg_type"."oid", "pg_attribute"."atttypmod") AS "db_type", pg_get_expr("pg_attrdef"."adbin", "pg_class"."oid") AS "default", NOT "pg_attribute"."attnotnull" AS "allow_null", COALESCE(("pg_attribute"."attnum" = ANY("pg_index"."indkey")), false) AS "primary_key", "pg_attribute"."attidentity", ("pg_attribute"."attgenerated" != '') AS "generated" FROM "pg_class" INNER JOIN "pg_attribute" ON ("pg_attribute"."attrelid" = "pg_class"."oid") INNER JOIN "pg_type" ON ("pg_type"."oid" = "pg_attribute"."atttypid") LEFT OUTER JOIN "pg_type" AS "basetype" ON ("basetype"."oid" = "pg_type"."typbasetype") LEFT OUTER JOIN "pg_attrdef" ON (("pg_attrdef"."adrelid" = "pg_class"."oid") AND ("pg_attrdef"."adnum" = "pg_attribute"."attnum")) LEFT OUTER JOIN "pg_index" ON (("pg_index"."indrelid" = "pg_class"."oid") AND ("pg_index"."indisprimary" IS TRUE)) WHERE (("pg_attribute"."attisdropped" IS FALSE) AND ("pg_attribute"."attnum" > 0) AND ("pg_class"."oid" = CAST(CAST('"third_party_integrations"' AS regclass) AS oid))) ORDER BY "pg_attribute"."attnum"`
added function call `controllers#find_preferred_mapsets`
  added function call `models#mapsets`
    added function call `models.ordered_list`
      added function call `models#ordered_mapset_dataset`
      added SQL `SELECT *, "mapsets"."name", "mapsets"."branch", "mapsets"."commit", "mapsets"."environment", "mapsets"."version", (SELECT "login" FROM "users" WHERE ("id" = "user_id")), (SELECT count(*) AS "scenario_count" FROM "scenarios" WHERE ("mapset_id" = "mapsets"."id")) FROM "mapsets_preference_ordered" AS "mapsets" WHERE ("mapsets"."app_id" = 1)`
changed SQL `SELECT * FROM (SELECT MAX("scanner_jobs"."id") AS "scanner_job_id", "app_id", 'current' AS "scanner_job_ref_type" FROM "scanner_jobs" INNER JOIN "mapsets" ON ("mapsets"."id" = "scanner_jobs"."mapset_id") INNER JOIN "apps" ON (("apps"."id" = "mapsets"."app_id") AND ("apps"."main_branch" = "mapsets"."branch")) WHERE ("apps"."id" = 43) GROUP BY "app_id") AS "scanner_jobs" LIMIT 1` to SQL `SELECT * FROM (WITH "scanner_job_counts" AS (SELECT "scanner_jobs"."id" AS "scanner_job_id", "sj_range"."app_id", "impact_category", "scanner_job_ref_type", COUNT(DISTINCT scanner_checks.id)::integer as rules_count, COUNT(finding_occurrences.*)::integer as findings_count, (COUNT(finding_occurrences.*) FILTER (WHERE status = 'new'))::integer as new_count, (COUNT(finding_occurrences.*) FILTER (WHERE status = 'deferred'))::integer as deferred_count FROM "scanner_jobs" INNER JOIN (SELECT * FROM (SELECT * FROM (SELECT MAX("scanner_jobs"."id") AS "scanner_job_id", "app_id", 'current' AS "scanner_job_ref_type" FROM "scanner_jobs" INNER JOIN "mapsets" ON ("mapsets"."id" = "scanner_jobs"."mapset_id") INNER JOIN "apps" ON (("apps"."id" = "mapsets"."app_id") AND ("apps"."main_branch" = "mapsets"."branch")) WHERE ("apps"."id" = 1) GROUP BY "app_id") AS "scanner_jobs" UNION (SELECT MAX("scanner_jobs"."id") AS "scanner_job_id", "app_id", 'base' AS "scanner_job_ref_type" FROM "scanner_jobs" INNER JOIN "mapsets" ON ("mapsets"."id" = "scanner_jobs"."mapset_id") INNER JOIN "apps" ON (("apps"."id" = "mapsets"."app_id") AND ("apps"."main_branch" = "mapsets"."branch")) WHERE ((scanner_jobs.created_at <= (NOW() - '604800 SECONDS'::INTERVAL)) AND ("apps"."id" = 1)) GROUP BY "app_id")) AS "t1") AS "sj_range" ON ("sj_range"."scanner_job_id" = "scanner_jobs"."id") INNER JOIN "scanner_checks" ON ("scanner_checks"."job_id" = "scanner_jobs"."id") LEFT JOIN "finding_occurrences" ON (("finding_occurrences"."check_id" = "scanner_checks"."id") AND ("finding_occurrences"."job_id" = "scanner_jobs"."id")) LEFT JOIN "scanner_finding_statuses" ON ("scanner_finding_statuses"."id" = "finding_occurrences"."status_id") WHERE ("impact_category" IS NOT NULL) GROUP BY "scanner_jobs"."id", "impact_category", "scanner_job_ref_type", "sj_range"."app_id" ORDER BY "scanner_jobs"."id", "impact_category", "scanner_job_ref_type", "sj_range"."app_id") SELECT coalesce("current"."impact_category", "base"."impact_category") AS "impact_category", "base"."rules_count"...`
2 times: removed SQL `SELECT * FROM (WITH "scanner_job_counts" AS (SELECT "scanner_jobs"."id" AS "scanner_job_id", "sj_range"."app_id", "impact_category", "scanner_job_ref_type", COUNT(DISTINCT scanner_checks.id)::integer as rules_count, COUNT(finding_occurrences.*)::integer as findings_count, (COUNT(finding_occurrences.*) FILTER (WHERE status = 'new'))::integer as new_count, (COUNT(finding_occurrences.*) FILTER (WHERE status = 'deferred'))::integer as deferred_count FROM "scanner_jobs" INNER JOIN (SELECT * FROM (SELECT * FROM (SELECT MAX("scanner_jobs"."id") AS "scanner_job_id", "app_id", 'current' AS "scanner_job_ref_type" FROM "scanner_jobs" INNER JOIN "mapsets" ON ("mapsets"."id" = "scanner_jobs"."mapset_id") INNER JOIN "apps" ON (("apps"."id" = "mapsets"."app_id") AND ("apps"."main_branch" = "mapsets"."branch")) WHERE ("apps"."id" = 43) GROUP BY "app_id") AS "scanner_jobs" UNION (SELECT MAX("scanner_jobs"."id") AS "scanner_job_id", "app_id", 'base' AS "scanner_job_ref_type" FROM "scanner_jobs" INNER JOIN "mapsets" ON ("mapsets"."id" = "scanner_jobs"."mapset_id") INNER JOIN "apps" ON (("apps"."id" = "mapsets"."app_id") AND ("apps"."main_branch" = "mapsets"."branch")) WHERE ((scanner_jobs.created_at <= (NOW() - '604800 SECONDS'::INTERVAL)) AND ("apps"."id" = 43)) GROUP BY "app_id")) AS "t1") AS "sj_range" ON ("sj_range"."scanner_job_id" = "scanner_jobs"."id") INNER JOIN "scanner_checks" ON ("scanner_checks"."job_id" = "scanner_jobs"."id") LEFT JOIN "finding_occurrences" ON (("finding_occurrences"."check_id" = "scanner_checks"."id") AND ("finding_occurrences"."job_id" = "scanner_jobs"."id")) LEFT JOIN "scanner_finding_statuses" ON ("scanner_finding_statuses"."id" = "finding_occurrences"."status_id") WHERE ("impact_category" IS NOT NULL) GROUP BY "scanner_jobs"."id", "impact_category", "scanner_job_ref_type", "sj_range"."app_id" ORDER BY "scanner_jobs"."id", "impact_category", "scanner_job_ref_type", "sj_range"."app_id") SELECT coalesce("current"."impact_category", "base"."impact_category") AS "impact_category", "base"."rules_coun...`
2 times: 
2 times:

Early access restrictions as admin provides the user with access to the expected pages

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment