diff --git a/Trigger/TrigValidation/TrigAnalysisTest/python/TrigAnalysisSteps.py b/Trigger/TrigValidation/TrigAnalysisTest/python/TrigAnalysisSteps.py
index 247d9d51b9d9deeb8c1aa757418735a8cd186080..1d0eede3af941e6bb77083bd52eef70cc07d5055 100644
--- a/Trigger/TrigValidation/TrigAnalysisTest/python/TrigAnalysisSteps.py
+++ b/Trigger/TrigValidation/TrigAnalysisTest/python/TrigAnalysisSteps.py
@@ -7,8 +7,11 @@ Definitions of additional validation steps in Trigger ART tests relevant only fo
 The main common check steps are defined in the TrigValSteering.CheckSteps module.
 '''
 
+from TrigValTools.TrigValSteering.Step import Step
 from TrigValTools.TrigValSteering.ExecStep import ExecStep
 from TrigValTools.TrigValSteering.CheckSteps import CheckFileStep, InputDependentStep, LogMergeStep
+import os
+import re
 
 ##################################################
 # Additional exec steps
@@ -66,6 +69,92 @@ class CheckFileTrigSizeStep(CheckFileStep):
         self.input_file = 'AOD.pool.root,ESD.pool.root,RDO_TRIG.pool.root'
         self.executable = 'checkFileTrigSize_RTT.py'
 
+class PhysValWebStep(Step):
+    '''
+    Execute physval_make_web_display.py to make PhysVal web display from NTUP_PHYSVAL.root
+    '''
+    def __init__(self, name='PhysValWeb'):
+        super(PhysValWebStep, self).__init__(name)
+        self.input_file = 'NTUP_PHYSVAL.pool.root'
+        self.executable = 'physval_make_web_display.py'
+        self.refdir = None
+        self.sig = None
+        self.args = '--ratio --drawopt HISTPE --refdrawopt HIST --title Test '
+        self.auto_report_result = True
+        self.timeout = 30*60
+        self.required = True  # whether the full test should fail if physval_make_web_display fails
+        self.required_no_red = False  # whether the full test should fail if red histograms are found
+
+    def configure(self, test):
+        assert self.sig, 'sig is a required parameter'
+        outargs = ' --outdir PHYSVAL_WEB/'+self.sig
+        dirargs = ' --startpath run_1/HLT/'+self.sig
+        self.args += ' '+outargs+' '+dirargs
+        super(PhysValWebStep, self).configure(test)
+
+    def get_refdir(self):
+        if self.refdir:
+            return
+        for fname in os.listdir('.'):
+            if fname.startswith('ref-') and os.path.isdir(fname):
+                self.refdir = fname
+
+    def run(self, dry_run=False):
+        if not dry_run and not os.path.exists(self.input_file):
+            self.log.error('Input file does not exist: %s', self.input_file)
+            self.result = 1
+            if self.auto_report_result:
+                self.report_result()
+            return self.result, '# (internal) {} -> failed'.format(self.name)
+
+        if not os.path.exists('PHYSVAL_WEB'):
+            os.mkdir('PHYSVAL_WEB')
+
+        self.get_refdir()
+        if self.refdir:
+            ref_file = self.refdir+'/'+self.input_file
+            if not dry_run and not os.path.exists(ref_file):
+                self.log.warning('Reference file %s does not exist, running without reference', ref_file)
+            else:
+                self.args += ' --reffile Ref:'+ref_file
+        self.args += ' '+self.input_file
+
+        do_report_result = self.auto_report_result
+        self.auto_report_result = False  # don't report yet, only after further checks below
+        self.result, cmd = super(PhysValWebStep, self).run(dry_run)
+        cmd += ' # Plus internal post-exec checks'
+        if dry_run:
+            if do_report_result:
+                self.report_result()
+            return self.result, cmd
+
+        fname = 'PHYSVAL_WEB/'+self.sig+'/index.html'
+        if not os.path.exists(fname):
+            self.log.error('Missing output file %s', fname)
+            self.result += 1000
+            if do_report_result:
+                self.report_result()
+            return self.result, cmd
+
+        nred_all = 0
+        with open(fname, 'r') as f:
+            red_lines = re.findall('Red.*$', f.read(), re.MULTILINE)
+            nred = len(red_lines)
+            nred_all += nred
+            if nred > 0:
+                msg = 'Red histograms in display for slice %s: %d'
+                if self.required_no_red:
+                    self.log.error(msg, self.sig, nred)
+                else:
+                    self.log.info(msg, self.sig, nred)
+
+        if do_report_result:
+            self.report_result(self.result + nred_all)
+        if self.required_no_red:
+            self.result += nred_all
+
+        return self.result, cmd
+
 ##################################################
 # Getter functions
 ##################################################
@@ -86,13 +175,28 @@ def add_analysis_steps(test, input_file='AOD.pool.root'):
     test.check_steps.extend(trig_analysis_check_steps())
 
     # Add the analysis exec step logs for merging
-    logmerge = None
-    for step in test.check_steps:
-        if type(step) == LogMergeStep:
-            logmerge = step
-            break
+    logmerge = test.get_step_by_type(LogMergeStep)
     if not logmerge:
         test.log.warning('LogMerge step not found, cannot add TrigAnalysisSteps exec step log files for merging')
     else:
         for step in analysis_exec_steps:
             logmerge.log_files.append(step.get_log_file_name())
+
+def add_physvalweb_steps(test, slice_names, download_step=None):
+    # Collect the steps
+    steps = [download_step] if download_step else []
+    for slice_name in slice_names:
+        sliceweb = PhysValWebStep('PhysValWeb'+slice_name)
+        sliceweb.sig = slice_name
+        steps.append(sliceweb)
+
+    # Add the steps at the beginning of check_steps list
+    test.check_steps = steps + test.check_steps
+
+    # Add the step logs for merging
+    logmerge = test.get_step_by_type(LogMergeStep)
+    if not logmerge:
+        test.log.warning('LogMerge step not found, cannot add PhysValWeb step log files for merging')
+    else:
+        for step in steps:
+            logmerge.log_files.append(step.get_log_file_name())
diff --git a/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_grid.py b/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_grid.py
index ad5e54d785513008a52f6d33eef553b206c31de0..70d873a2f3456063ee91ff8e8a329719c1ea2c09 100755
--- a/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_grid.py
+++ b/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_grid.py
@@ -23,6 +23,7 @@
 # art-html: PHYSVAL_WEB
 
 from TrigValTools.TrigValSteering import Test, ExecStep, CheckSteps
+from TrigAnalysisTest.TrigAnalysisSteps import add_physvalweb_steps
 import os
 
 # To run single-process transform on MCORE sites
@@ -50,38 +51,13 @@ test.art_type = 'grid'
 test.exec_steps = [rdo2aod,physval]
 test.check_steps = CheckSteps.default_check_steps(test)
 
+# Add web display steps
+slice_names = [
+    'JetMon', 'TauMon', 'MuonMon', 'IDMon',
+    'BphysMon', 'HLTCaloESD', 'ResultMon', 'BjetMon',
+    'METMon', 'MinBiasMon', 'Egamma']
+download = CheckSteps.DownloadRefStep()
+add_physvalweb_steps(test, slice_names, download)
 
-download=CheckSteps.DownloadRefStep()
-download.artpackage = 'TrigAnalysisTest'
-download.artjobname = 'test_trigAna_PhysValWeb_grid.py'
-download.required=True
-test.check_steps.append(download)
-
-
-if not os.path.exists('PHYSVAL_WEB'):
-    os.mkdir('PHYSVAL_WEB')
-
-
-pv=[]
-pv.append(['Jet','JetMon'])
-pv.append(['Tau','TauMon'])
-pv.append(['Muon','MuonMon'])
-pv.append(['ID','IDMon'])
-pv.append(['Bphys','BphysMon'])
-pv.append(['HLTCalo','HLTCaloESD'])
-pv.append(['Result','ResultMon'])
-pv.append(['Bjet','BjetMon'])
-pv.append(['MET','METMon'])
-pv.append(['MinBias','MinBiasMon'])
-pv.append(['Egamma','Egamma'])
-
-for slice in pv:
-    name='PhysValWeb'+slice[0]
-    sliceweb=CheckSteps.PhysValWebStep(name)
-    sliceweb.sig=slice[1]
-    sliceweb.required=True
-    test.check_steps.append(sliceweb)
-    
 import sys
 sys.exit(test.run())
-
diff --git a/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_compLegacy_grid.py b/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_compLegacy_grid.py
index c7314633b91bdde0d0f912f49e14adc7705731b8..2aa9c0cfd7c2311bb14ee76119c0f060acf45553 100755
--- a/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_compLegacy_grid.py
+++ b/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_compLegacy_grid.py
@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 
-# art-description: Test of transform RDO->RDO_TRIG->ESD->AOD with AthenaMT and AOD->NTUP_PHYSVAL with serial athena to produce webdisplay (comparing with legacy menu)
+# art-description: Produce web display comparing Run-3 trigger to Run-2 (legacy) trigger using legacy monitoring NTUP_PHYSVAL from previous nightlies
 # art-type: grid
 # art-include: master/Athena
 # art-output: *.txt
@@ -22,67 +22,29 @@
 # art-output: PHYSVAL_WEB
 # art-html: PHYSVAL_WEB
 
-from TrigValTools.TrigValSteering import Test, ExecStep, CheckSteps
-import os
+from TrigValTools.TrigValSteering import Test, CheckSteps
+from TrigAnalysisTest.TrigAnalysisSteps import add_physvalweb_steps
 
-# To run single-process transform on MCORE sites
-if 'ATHENA_NPROC_NUM' in os.environ:
-    del os.environ['ATHENA_NPROC_NUM']
+downloadLegacyTrig = CheckSteps.DownloadRefStep('DownloadLegacyTriggerNTUP')
+downloadLegacyTrig.artjobname = 'test_trigAna_PhysValWeb_grid.py'
 
-rdo2aod = ExecStep.ExecStep('RDOtoAOD')
-rdo2aod.type = 'Reco_tf'
-rdo2aod.input = 'ttbar'
-rdo2aod.threads = 1
-rdo2aod.max_events = 500
-rdo2aod.args = '--outputAODFile=AOD.pool.root --steering="doRDO_TRIG" --valid=True'
-rdo2aod.args += ' --preExec="all:from TriggerJobOpts.TriggerFlags import TriggerFlags; TriggerFlags.AODEDMSet.set_Value_and_Lock(\\\"AODFULL\\\");"'
-
-physval = ExecStep.ExecStep('PhysVal')
-physval.type = 'Reco_tf'
-physval.input = ''
-physval.explicit_input = True
-physval.args = '--inputAODFile=AOD.pool.root --outputNTUP_PHYSVALFile=NTUP_PHYSVAL.pool.root --valid=True'
-
-validationFlags = 'doTrigEgamma,doTrigBphys,doTrigMET,doTrigJet,doTrigMuon,doTrigHLTResult,doTrigCalo,doTrigMinBias,doTrigTau,doTrigIDtrk,doTrigBjet'
-physval.args += ' --validationFlags="{:s}"'.format(validationFlags)
+downloadRun3Trig = CheckSteps.DownloadRefStep('DownloadRun3TriggerNTUP')
+downloadRun3Trig.artjobname = 'test_trigAna_PhysValWeb_mt1_grid.py'
+downloadRun3Trig.args += ' --dst="."'
 
 test = Test.Test()
 test.art_type = 'grid'
-test.exec_steps = [rdo2aod,physval]
-test.check_steps = CheckSteps.default_check_steps(test)
-
-
-download=CheckSteps.DownloadRefStep()
-download.artpackage = 'TrigAnalysisTest'
-download.artjobname = 'test_trigAna_PhysValWeb_grid.py'
-download.required=True
-test.check_steps.append(download)
-
+test.exec_steps = [downloadLegacyTrig,downloadRun3Trig]
+# Only keep relevant checks from the defaults
+test.check_steps = [chk for chk in CheckSteps.default_check_steps(test)
+                    if type(chk) in (CheckSteps.LogMergeStep, CheckSteps.CheckLogStep)]
+
+# Add web display steps
+slice_names = [
+    'JetMon', 'TauMon', 'MuonMon', 'IDMon',
+    'BphysMon', 'HLTCaloESD', 'ResultMon', 'BjetMon',
+    'METMon', 'MinBiasMon', 'Egamma']
+add_physvalweb_steps(test, slice_names)
 
-if not os.path.exists('PHYSVAL_WEB'):
-    os.mkdir('PHYSVAL_WEB')
-
-
-pv=[]
-pv.append(['Jet','JetMon'])
-pv.append(['Tau','TauMon'])
-pv.append(['Muon','MuonMon'])
-pv.append(['ID','IDMon'])
-pv.append(['Bphys','BphysMon'])
-pv.append(['HLTCalo','HLTCaloESD'])
-pv.append(['Result','ResultMon'])
-pv.append(['Bjet','BjetMon'])
-pv.append(['MET','METMon'])
-pv.append(['MinBias','MinBiasMon'])
-pv.append(['Egamma','Egamma'])
-
-for slice in pv:
-    name='PhysValWeb'+slice[0]
-    sliceweb=CheckSteps.PhysValWebStep(name)
-    sliceweb.sig=slice[1]
-    sliceweb.required=True
-    test.check_steps.append(sliceweb)
-    
 import sys
 sys.exit(test.run())
-
diff --git a/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_grid.py b/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_grid.py
index 4b6491272d040944e7949398babe8904747c772d..412ce230290ed8ad9c653765cf9135f6bf3ffefb 100755
--- a/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_grid.py
+++ b/Trigger/TrigValidation/TrigAnalysisTest/test/test_trigAna_PhysValWeb_mt1_grid.py
@@ -23,6 +23,7 @@
 # art-html: PHYSVAL_WEB
 
 from TrigValTools.TrigValSteering import Test, ExecStep, CheckSteps
+from TrigAnalysisTest.TrigAnalysisSteps import add_physvalweb_steps
 import os
 
 # To run single-process transform on MCORE sites
@@ -51,38 +52,13 @@ test.art_type = 'grid'
 test.exec_steps = [rdo2aod,physval]
 test.check_steps = CheckSteps.default_check_steps(test)
 
+# Add web display steps
+slice_names = [
+    'JetMon', 'TauMon', 'MuonMon', 'IDMon',
+    'BphysMon', 'HLTCaloESD', 'ResultMon', 'BjetMon',
+    'METMon', 'MinBiasMon', 'Egamma']
+download = CheckSteps.DownloadRefStep()
+add_physvalweb_steps(test, slice_names, download)
 
-download=CheckSteps.DownloadRefStep()
-download.artpackage = 'TrigAnalysisTest'
-download.artjobname = 'test_trigAna_PhysValWeb_mt1_grid.py'
-download.required=True
-test.check_steps.append(download)
-
-
-if not os.path.exists('PHYSVAL_WEB'):
-    os.mkdir('PHYSVAL_WEB')
-
-
-pv=[]
-pv.append(['Jet','JetMon'])
-pv.append(['Tau','TauMon'])
-pv.append(['Muon','MuonMon'])
-pv.append(['ID','IDMon'])
-pv.append(['Bphys','BphysMon'])
-pv.append(['HLTCalo','HLTCaloESD'])
-pv.append(['Result','ResultMon'])
-pv.append(['Bjet','BjetMon'])
-pv.append(['MET','METMon'])
-pv.append(['MinBias','MinBiasMon'])
-pv.append(['Egamma','Egamma'])
-
-for slice in pv:
-    name='PhysValWeb'+slice[0]
-    sliceweb=CheckSteps.PhysValWebStep(name)
-    sliceweb.sig=slice[1]
-    sliceweb.required=True
-    test.check_steps.append(sliceweb)
-    
 import sys
 sys.exit(test.run())
-
diff --git a/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/CheckSteps.py b/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/CheckSteps.py
index 563525ef541a6453e1d180c97271d2cf112170f7..624f35c396a4e0acb449bcbe5398d0c8c3dbd3c0 100644
--- a/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/CheckSteps.py
+++ b/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/CheckSteps.py
@@ -423,25 +423,29 @@ class TailStep(Step):
         self.args += ' >'+self.output_name
         super(TailStep, self).configure(test)
 
+
 class DownloadRefStep(Step):
-    '''Execute art.py download to downlaod results from previous day '''
+    '''Execute art.py download to get results from previous days'''
 
-    def __init__(self, name='DownloadRefWeb'):
+    def __init__(self, name='DownloadRef'):
         super(DownloadRefStep, self).__init__(name)
         self.executable = 'art.py'
-        self.artpackage = ' '
-        self.artjobname = ' '
-        self.args = 'download '
+        self.args = 'download'
+        self.artpackage = None
+        self.artjobname = None
         self.timeout = 20*60
         self.required = True
         self.auto_report_result = True
 
     def configure(self, test):
+        if not self.artpackage:
+            self.artpackage = test.package_name
+        if not self.artjobname:
+            self.artjobname = 'test_'+test.name+'.py'
         self.args += ' '+self.artpackage+' '+self.artjobname
         super(DownloadRefStep, self).configure(test)
 
 
-
 class HistCountStep(InputDependentStep):
     '''Execute histSizes.py to count histograms in a ROOT file'''
 
@@ -456,50 +460,6 @@ class HistCountStep(InputDependentStep):
         super(HistCountStep, self).configure(test)
 
 
-class PhysValWebStep(InputDependentStep):
-    '''Execute physval_make_web_display.py to make PhysVal web display from NTUP_PHYSVAL.root'''
-
-    def __init__(self, name='PhysValWeb'):
-        super(PhysValWebStep, self).__init__(name)
-        self.input_file = 'NTUP_PHYSVAL.pool.root'
-        self.executable = 'physval_make_web_display.py'
-        self.refdir = ' '
-        self.sig=' '
-        self.args = '--ratio --drawopt HISTPE --refdrawopt HIST --title Test '
-        self.auto_report_result = True
-        self.timeout = 30*60
-        self.required = True
-        
-    def configure(self, test):
-        outargs = ' --outdir PHYSVAL_WEB/'+self.sig
-        dirargs = ' --startpath run_1/HLT/'+self.sig
-        self.args += ' '+outargs+' '+dirargs
-        super(PhysValWebStep, self).configure(test)
-
-    def run(self, dry_run=False):
-        for fname in os.listdir('.'):
-            if fname.startswith('ref-'): 
-                self.refdir = fname
-        refargs = ' --reffile Ref:'+self.refdir+'/NTUP_PHYSVAL.pool.root '
-        self.args += ' '+refargs+' '+self.input_file
-        retcode, cmd = super(PhysValWebStep, self).run(dry_run)
-        fname='PHYSVAL_WEB/'+self.sig+'/index.html'
-        if os.path.exists(fname):
-            f=open(fname,"r")
-            nred=0
-            for line in f:
-                if (line.find('Red') != -1):
-                    nred+=1
-            if nred > 0:
-                self.log.debug("red histograms in display for slice %s %d",self.sig,nred)
-                retcode+=nred
-        else:
-            retcode+=1000
-            self.log.debug("missing index.html file for slice: %s ",self.sig)
-        self.report_result(retcode,"CheckWeb"+self.sig)
-        return retcode, cmd
-
-
 class ChainDumpStep(InputDependentStep):
     '''
     Execute chainDump.py to print trigger counts from histograms to text files
diff --git a/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Step.py b/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Step.py
index d8348957484ac6c7f5c69325d78ba842e9bab711..58bcaf906843d30d39bef4365ec1a50ee7984bb9 100644
--- a/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Step.py
+++ b/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Step.py
@@ -219,3 +219,14 @@ def get_step_from_list(step_name, step_list):
         if step.name is not None and step_name in step.name:
             return step
     return None
+
+
+def get_step_type_from_list(step_type, step_list):
+    '''
+    Retrieve the first test matching the type from the list. Returns None if
+    no match is found.
+    '''
+    for step in step_list:
+        if isinstance(step, step_type):
+            return step
+    return None
diff --git a/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Test.py b/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Test.py
index 99127722069aaa181af6a0eb550710201482761f..9edb3268b47006bcdf479f4c21aeebd0e2e1f762 100644
--- a/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Test.py
+++ b/Trigger/TrigValidation/TrigValTools/python/TrigValSteering/Test.py
@@ -13,7 +13,7 @@ import subprocess
 from collections import OrderedDict
 
 from TrigValTools.TrigValSteering.Common import get_logger, art_result, clear_art_summary, package_prefix_dict
-from TrigValTools.TrigValSteering.Step import get_step_from_list
+from TrigValTools.TrigValSteering.Step import get_step_from_list, get_step_type_from_list
 
 
 class Test(object):
@@ -196,6 +196,12 @@ class Test(object):
             step = get_step_from_list(step_name, self.check_steps)
         return step
 
+    def get_step_by_type(self, step_type):
+        step = get_step_type_from_list(step_type, self.exec_steps)
+        if step is None:
+            step = get_step_type_from_list(step_type, self.check_steps)
+        return step
+
     def pre_exec(self):
         '''Extra pre-exec function executed just before the steps'''
         cmd_list = []