diff --git a/LICENSE.txt b/LICENSE.txt
index 1e94e57b2b7320e099d72a3911c5270ae196d878..60ab3ac6f115e10957efeb150d5a1a87837245d1 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -316,6 +316,10 @@ Applications:
    See licenses/LICENSE-NDT.txt
    Notice: I2P has changed specified portions of the Software, including the package edu.internet2.ndt.
 
+   Router Console Iframe-resizer 4.3.9:
+   Copyright (c) 2013-2023 David J. Bradshaw
+   See licenses/LICENSE-Iframe-resizer.txt
+
    SAM (sam.jar):
    Public domain.
 
diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
index 35ec8d23c9d7ae63f25f9c6afaef5ee4909cb173..d13ecb362d3b6e41100fb5c36c8cf1cf703508ce 100644
--- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
+++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java
@@ -348,6 +348,7 @@ public class I2PSnarkServlet extends BasicServlet {
                       "<script src=\".resources/js/delete.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>\n" +
                       "<script src=\".resources/js/search.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>\n");
         }
+        out.write("<script src=\"/js/iframeResizer.contentWindow.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>\n");
         out.write(HEADER_A + _themePath + HEADER_B);
 
         //  ...and inject CSS to display panels uncollapsed
diff --git a/apps/i2ptunnel/jsp/edit.jsp b/apps/i2ptunnel/jsp/edit.jsp
index c23401891f4a6f0b788496802c3bd04158d55494..dba8f86c0034636ef8630e3d1e15de0f5a77c6e4 100644
--- a/apps/i2ptunnel/jsp/edit.jsp
+++ b/apps/i2ptunnel/jsp/edit.jsp
@@ -44,6 +44,7 @@ if (tun != null) {
   input.default { width: 1px; height: 1px; visibility: hidden; }
 </style>
 <script src="/js/resetScroll.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
+<script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <script src="js/tableSlider.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <script nonce="<%=cspNonce%>" type="text/javascript">
   var deleteMessage = "<%=intl._t("Are you sure you want to delete?")%>";
diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp
index 39b38ba3ab389022f69584da20c38044569b27b3..710cc292be29cc4c5597bdad7888d0938162836d 100644
--- a/apps/i2ptunnel/jsp/index.jsp
+++ b/apps/i2ptunnel/jsp/index.jsp
@@ -14,6 +14,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <link href="/themes/console/images/favicon.ico" type="image/x-icon" rel="shortcut icon" />
     <link href="<%=indexBean.getTheme()%>i2ptunnel.css?<%=net.i2p.CoreVersion.VERSION%>" rel="stylesheet" type="text/css" /> 
+    <script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
     <script src="js/copy.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
     <noscript><style> .jsonly { display: none } </style></noscript>
 </head><body id="tunnelListPage">
diff --git a/apps/i2ptunnel/jsp/register.jsp b/apps/i2ptunnel/jsp/register.jsp
index 3051bb0cddb924f50a6d1066771f108bc1a6c938..18f4083a91cee7453add521062bfcaed674316b1 100644
--- a/apps/i2ptunnel/jsp/register.jsp
+++ b/apps/i2ptunnel/jsp/register.jsp
@@ -28,6 +28,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <link href="/themes/console/images/favicon.ico" type="image/x-icon" rel="shortcut icon" />
     <link href="<%=editBean.getTheme()%>i2ptunnel.css?<%=net.i2p.CoreVersion.VERSION%>" rel="stylesheet" type="text/css" /> 
+    <script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <style type='text/css'>
 input.default { width: 1px; height: 1px; visibility: hidden; }
 </style>
diff --git a/apps/i2ptunnel/jsp/ssl.jsp b/apps/i2ptunnel/jsp/ssl.jsp
index 1b91e4a0015ea587ee3dbc0194d5004841088eb9..b60d5138029cabb139fbb62d319aef5d6ad849bf 100644
--- a/apps/i2ptunnel/jsp/ssl.jsp
+++ b/apps/i2ptunnel/jsp/ssl.jsp
@@ -26,6 +26,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <link href="/themes/console/images/favicon.ico" type="image/x-icon" rel="shortcut icon" />
     <link href="<%=editBean.getTheme()%>i2ptunnel.css?<%=net.i2p.CoreVersion.VERSION%>" rel="stylesheet" type="text/css" /> 
+    <script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <style type='text/css'>
 input.default { width: 1px; height: 1px; visibility: hidden; }
 </style>
diff --git a/apps/i2ptunnel/jsp/wizard.jsp b/apps/i2ptunnel/jsp/wizard.jsp
index 7640243e31fd736b2c8b7a644bac5853c5e08bc5..d054ff193802dcc43b5cd594c0d9ca34c349674a 100644
--- a/apps/i2ptunnel/jsp/wizard.jsp
+++ b/apps/i2ptunnel/jsp/wizard.jsp
@@ -46,6 +46,7 @@
     <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
     <link href="/themes/console/images/favicon.ico" type="image/x-icon" rel="shortcut icon" />
     <link href="<%=editBean.getTheme()%>i2ptunnel.css?<%=net.i2p.CoreVersion.VERSION%>" rel="stylesheet" type="text/css" />
+    <script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 </head>
 <body id="tunnelWizardPage">
     <form method="post" action="<%=(curPage == 7 ? "list" : "wizard") %>">
diff --git a/apps/routerconsole/jsp/dns.jsp b/apps/routerconsole/jsp/dns.jsp
index 1c717f16b9890840cd0b3a7e8a5a7cdbf69a0b6f..062c715de4912ce4ea1a7e2b7fe76b7eefa1c690 100644
--- a/apps/routerconsole/jsp/dns.jsp
+++ b/apps/routerconsole/jsp/dns.jsp
@@ -25,6 +25,7 @@
 <%@include file="css.jsi" %>
 <%=intl.title("Address Book")%>
 <script src="/js/iframed.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
+<script src="/js/iframeResizer.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <%@include file="summaryajax.jsi" %>
 <script nonce="<%=cspNonce%>" type="text/javascript">
 /* @license http://creativecommons.org/publicdomain/zero/1.0/legalcode CC0-1.0 */
@@ -34,6 +35,7 @@
       f.addEventListener("load", function() {
           injectClass(f);
           resizeFrame(f);
+          iFrameResize({ log: false }, '#susidnsframe')
       }, true);
   }
 
diff --git a/apps/routerconsole/jsp/i2ptunnelmgr.jsp b/apps/routerconsole/jsp/i2ptunnelmgr.jsp
index dd2969f3aa67aa5ba8447241842ff4e91a88fc28..779ce7ed8e9d407e9877b94661f6da77c939988f 100644
--- a/apps/routerconsole/jsp/i2ptunnelmgr.jsp
+++ b/apps/routerconsole/jsp/i2ptunnelmgr.jsp
@@ -25,6 +25,7 @@
 <%@include file="css.jsi" %>
 <%=intl.title("Hidden Services Manager")%>
 <script src="/js/iframed.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
+<script src="/js/iframeResizer.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <%@include file="summaryajax.jsi" %>
 <script nonce="<%=cspNonce%>" type="text/javascript">
 /* @license http://creativecommons.org/publicdomain/zero/1.0/legalcode CC0-1.0 */
@@ -56,6 +57,7 @@
           injectClass(f);
           injectClassSpecific(f);
           resizeFrame(f);
+          iFrameResize({ log: false }, '#i2ptunnelframe')
       }, true);
   }
 
diff --git a/apps/routerconsole/jsp/js/iframeResizer.contentWindow.js b/apps/routerconsole/jsp/js/iframeResizer.contentWindow.js
new file mode 100644
index 0000000000000000000000000000000000000000..d32844ef35724f44e302df706c0b2a499ff188f4
--- /dev/null
+++ b/apps/routerconsole/jsp/js/iframeResizer.contentWindow.js
@@ -0,0 +1,1305 @@
+/*
+ * File: iframeResizer.contentWindow.js
+ * Desc: Include this file in any page being loaded into an iframe
+ *       to force the iframe to resize to the content size.
+ * Requires: iframeResizer.js on host page.
+ * Doc: https://github.com/davidjbradshaw/iframe-resizer
+ * Author: David J. Bradshaw - dave@bradshaw.net
+ *
+ */
+
+// eslint-disable-next-line sonarjs/cognitive-complexity, no-shadow-restricted-names
+;(function (undefined) {
+  if (typeof window === 'undefined') return // don't run for server side render
+
+  var autoResize = true,
+    base = 10,
+    bodyBackground = '',
+    bodyMargin = 0,
+    bodyMarginStr = '',
+    bodyObserver = null,
+    bodyPadding = '',
+    calculateWidth = false,
+    doubleEventList = { resize: 1, click: 1 },
+    eventCancelTimer = 128,
+    firstRun = true,
+    height = 1,
+    heightCalcModeDefault = 'bodyOffset',
+    heightCalcMode = heightCalcModeDefault,
+    initLock = true,
+    initMsg = '',
+    inPageLinks = {},
+    interval = 32,
+    intervalTimer = null,
+    logging = false,
+    mouseEvents = false,
+    msgID = '[iFrameSizer]', // Must match host page msg ID
+    msgIdLen = msgID.length,
+    myID = '',
+    resetRequiredMethods = {
+      max: 1,
+      min: 1,
+      bodyScroll: 1,
+      documentElementScroll: 1
+    },
+    resizeFrom = 'child',
+    sendPermit = true,
+    target = window.parent,
+    targetOriginDefault = '*',
+    tolerance = 0,
+    triggerLocked = false,
+    triggerLockedTimer = null,
+    throttledTimer = 16,
+    width = 1,
+    widthCalcModeDefault = 'scroll',
+    widthCalcMode = widthCalcModeDefault,
+    win = window,
+    onMessage = function () {
+      warn('onMessage function not defined')
+    },
+    onReady = function () {},
+    onPageInfo = function () {},
+    customCalcMethods = {
+      height: function () {
+        warn('Custom height calculation function not defined')
+        return document.documentElement.offsetHeight
+      },
+      width: function () {
+        warn('Custom width calculation function not defined')
+        return document.body.scrollWidth
+      }
+    },
+    eventHandlersByName = {},
+    passiveSupported = false
+
+  function noop() {}
+
+  try {
+    var options = Object.create(
+      {},
+      {
+        passive: {
+          // eslint-disable-next-line getter-return
+          get: function () {
+            passiveSupported = true
+          }
+        }
+      }
+    )
+    window.addEventListener('test', noop, options)
+    window.removeEventListener('test', noop, options)
+  } catch (error) {
+    /* */
+  }
+
+  function addEventListener(el, evt, func, options) {
+    el.addEventListener(evt, func, passiveSupported ? options || {} : false)
+  }
+
+  function removeEventListener(el, evt, func) {
+    el.removeEventListener(evt, func, false)
+  }
+
+  function capitalizeFirstLetter(string) {
+    return string.charAt(0).toUpperCase() + string.slice(1)
+  }
+
+  // Based on underscore.js
+  function throttle(func) {
+    var context,
+      args,
+      result,
+      timeout = null,
+      previous = 0,
+      later = function () {
+        previous = Date.now()
+        timeout = null
+        result = func.apply(context, args)
+        if (!timeout) {
+          // eslint-disable-next-line no-multi-assign
+          context = args = null
+        }
+      }
+
+    return function () {
+      var now = Date.now()
+
+      if (!previous) {
+        previous = now
+      }
+
+      var remaining = throttledTimer - (now - previous)
+
+      context = this
+      args = arguments
+
+      if (remaining <= 0 || remaining > throttledTimer) {
+        if (timeout) {
+          clearTimeout(timeout)
+          timeout = null
+        }
+
+        previous = now
+        result = func.apply(context, args)
+
+        if (!timeout) {
+          // eslint-disable-next-line no-multi-assign
+          context = args = null
+        }
+      } else if (!timeout) {
+        timeout = setTimeout(later, remaining)
+      }
+
+      return result
+    }
+  }
+
+  function formatLogMsg(msg) {
+    return msgID + '[' + myID + '] ' + msg
+  }
+
+  function log(msg) {
+    if (logging && 'object' === typeof window.console) {
+      // eslint-disable-next-line no-console
+      console.log(formatLogMsg(msg))
+    }
+  }
+
+  function warn(msg) {
+    if ('object' === typeof window.console) {
+      // eslint-disable-next-line no-console
+      console.warn(formatLogMsg(msg))
+    }
+  }
+
+  function init() {
+    readDataFromParent()
+    log('Initialising iFrame (' + window.location.href + ')')
+    readDataFromPage()
+    setMargin()
+    setBodyStyle('background', bodyBackground)
+    setBodyStyle('padding', bodyPadding)
+    injectClearFixIntoBodyElement()
+    checkHeightMode()
+    checkWidthMode()
+    stopInfiniteResizingOfIFrame()
+    setupPublicMethods()
+    setupMouseEvents()
+    startEventListeners()
+    inPageLinks = setupInPageLinks()
+    sendSize('init', 'Init message from host page')
+    onReady()
+  }
+
+  function readDataFromParent() {
+    function strBool(str) {
+      return 'true' === str
+    }
+
+    var data = initMsg.slice(msgIdLen).split(':')
+
+    myID = data[0]
+    bodyMargin = undefined === data[1] ? bodyMargin : Number(data[1]) // For V1 compatibility
+    calculateWidth = undefined === data[2] ? calculateWidth : strBool(data[2])
+    logging = undefined === data[3] ? logging : strBool(data[3])
+    interval = undefined === data[4] ? interval : Number(data[4])
+    autoResize = undefined === data[6] ? autoResize : strBool(data[6])
+    bodyMarginStr = data[7]
+    heightCalcMode = undefined === data[8] ? heightCalcMode : data[8]
+    bodyBackground = data[9]
+    bodyPadding = data[10]
+    tolerance = undefined === data[11] ? tolerance : Number(data[11])
+    inPageLinks.enable = undefined === data[12] ? false : strBool(data[12])
+    resizeFrom = undefined === data[13] ? resizeFrom : data[13]
+    widthCalcMode = undefined === data[14] ? widthCalcMode : data[14]
+    mouseEvents = undefined === data[15] ? mouseEvents : strBool(data[15])
+  }
+
+  function depricate(key) {
+    var splitName = key.split('Callback')
+
+    if (splitName.length === 2) {
+      var name =
+        'on' + splitName[0].charAt(0).toUpperCase() + splitName[0].slice(1)
+      this[name] = this[key]
+      delete this[key]
+      warn(
+        "Deprecated: '" +
+          key +
+          "' has been renamed '" +
+          name +
+          "'. The old method will be removed in the next major version."
+      )
+    }
+  }
+
+  function readDataFromPage() {
+    function readData() {
+      var data = window.iFrameResizer
+
+      log('Reading data from page: ' + JSON.stringify(data))
+      Object.keys(data).forEach(depricate, data)
+
+      onMessage = 'onMessage' in data ? data.onMessage : onMessage
+      onReady = 'onReady' in data ? data.onReady : onReady
+      targetOriginDefault =
+        'targetOrigin' in data ? data.targetOrigin : targetOriginDefault
+      heightCalcMode =
+        'heightCalculationMethod' in data
+          ? data.heightCalculationMethod
+          : heightCalcMode
+      widthCalcMode =
+        'widthCalculationMethod' in data
+          ? data.widthCalculationMethod
+          : widthCalcMode
+    }
+
+    function setupCustomCalcMethods(calcMode, calcFunc) {
+      if ('function' === typeof calcMode) {
+        log('Setup custom ' + calcFunc + 'CalcMethod')
+        customCalcMethods[calcFunc] = calcMode
+        calcMode = 'custom'
+      }
+
+      return calcMode
+    }
+
+    if (
+      'iFrameResizer' in window &&
+      Object === window.iFrameResizer.constructor
+    ) {
+      readData()
+      heightCalcMode = setupCustomCalcMethods(heightCalcMode, 'height')
+      widthCalcMode = setupCustomCalcMethods(widthCalcMode, 'width')
+    }
+
+    log('TargetOrigin for parent set to: ' + targetOriginDefault)
+  }
+
+  function chkCSS(attr, value) {
+    if (-1 !== value.indexOf('-')) {
+      warn('Negative CSS value ignored for ' + attr)
+      value = ''
+    }
+    return value
+  }
+
+  function setBodyStyle(attr, value) {
+    if (undefined !== value && '' !== value && 'null' !== value) {
+      document.body.style[attr] = value
+      log('Body ' + attr + ' set to "' + value + '"')
+    }
+  }
+
+  function setMargin() {
+    // If called via V1 script, convert bodyMargin from int to str
+    if (undefined === bodyMarginStr) {
+      bodyMarginStr = bodyMargin + 'px'
+    }
+
+    setBodyStyle('margin', chkCSS('margin', bodyMarginStr))
+  }
+
+  function stopInfiniteResizingOfIFrame() {
+    document.documentElement.style.height = ''
+    document.body.style.height = ''
+    log('HTML & body height set to "auto"')
+  }
+
+  function manageTriggerEvent(options) {
+    var listener = {
+      add: function (eventName) {
+        function handleEvent() {
+          sendSize(options.eventName, options.eventType)
+        }
+
+        eventHandlersByName[eventName] = handleEvent
+
+        addEventListener(window, eventName, handleEvent, { passive: true })
+      },
+      remove: function (eventName) {
+        var handleEvent = eventHandlersByName[eventName]
+        delete eventHandlersByName[eventName]
+
+        removeEventListener(window, eventName, handleEvent)
+      }
+    }
+
+    if (options.eventNames && Array.prototype.map) {
+      options.eventName = options.eventNames[0]
+      options.eventNames.map(listener[options.method])
+    } else {
+      listener[options.method](options.eventName)
+    }
+
+    log(
+      capitalizeFirstLetter(options.method) +
+        ' event listener: ' +
+        options.eventType
+    )
+  }
+
+  function manageEventListeners(method) {
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Animation Start',
+      eventNames: ['animationstart', 'webkitAnimationStart']
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Animation Iteration',
+      eventNames: ['animationiteration', 'webkitAnimationIteration']
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Animation End',
+      eventNames: ['animationend', 'webkitAnimationEnd']
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Input',
+      eventName: 'input'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Mouse Up',
+      eventName: 'mouseup'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Mouse Down',
+      eventName: 'mousedown'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Orientation Change',
+      eventName: 'orientationchange'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Print',
+      eventNames: ['afterprint', 'beforeprint']
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Ready State Change',
+      eventName: 'readystatechange'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Touch Start',
+      eventName: 'touchstart'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Touch End',
+      eventName: 'touchend'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Touch Cancel',
+      eventName: 'touchcancel'
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Transition Start',
+      eventNames: [
+        'transitionstart',
+        'webkitTransitionStart',
+        'MSTransitionStart',
+        'oTransitionStart',
+        'otransitionstart'
+      ]
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Transition Iteration',
+      eventNames: [
+        'transitioniteration',
+        'webkitTransitionIteration',
+        'MSTransitionIteration',
+        'oTransitionIteration',
+        'otransitioniteration'
+      ]
+    })
+    manageTriggerEvent({
+      method: method,
+      eventType: 'Transition End',
+      eventNames: [
+        'transitionend',
+        'webkitTransitionEnd',
+        'MSTransitionEnd',
+        'oTransitionEnd',
+        'otransitionend'
+      ]
+    })
+    if ('child' === resizeFrom) {
+      manageTriggerEvent({
+        method: method,
+        eventType: 'IFrame Resized',
+        eventName: 'resize'
+      })
+    }
+  }
+
+  function checkCalcMode(calcMode, calcModeDefault, modes, type) {
+    if (calcModeDefault !== calcMode) {
+      if (!(calcMode in modes)) {
+        warn(
+          calcMode + ' is not a valid option for ' + type + 'CalculationMethod.'
+        )
+        calcMode = calcModeDefault
+      }
+      log(type + ' calculation method set to "' + calcMode + '"')
+    }
+
+    return calcMode
+  }
+
+  function checkHeightMode() {
+    heightCalcMode = checkCalcMode(
+      heightCalcMode,
+      heightCalcModeDefault,
+      getHeight,
+      'height'
+    )
+  }
+
+  function checkWidthMode() {
+    widthCalcMode = checkCalcMode(
+      widthCalcMode,
+      widthCalcModeDefault,
+      getWidth,
+      'width'
+    )
+  }
+
+  function startEventListeners() {
+    if (true === autoResize) {
+      manageEventListeners('add')
+      setupMutationObserver()
+    } else {
+      log('Auto Resize disabled')
+    }
+  }
+
+  //   function stopMsgsToParent() {
+  //     log('Disable outgoing messages')
+  //     sendPermit = false
+  //   }
+
+  //   function removeMsgListener() {
+  //     log('Remove event listener: Message')
+  //     removeEventListener(window, 'message', receiver)
+  //   }
+
+  function disconnectMutationObserver() {
+    if (null !== bodyObserver) {
+      /* istanbul ignore next */ // Not testable in PhantonJS
+      bodyObserver.disconnect()
+    }
+  }
+
+  function stopEventListeners() {
+    manageEventListeners('remove')
+    disconnectMutationObserver()
+    clearInterval(intervalTimer)
+  }
+
+  //   function teardown() {
+  //     stopMsgsToParent()
+  //     removeMsgListener()
+  //     if (true === autoResize) stopEventListeners()
+  //   }
+
+  function injectClearFixIntoBodyElement() {
+    var clearFix = document.createElement('div')
+    clearFix.style.clear = 'both'
+    // Guard against the following having been globally redefined in CSS.
+    clearFix.style.display = 'block'
+    clearFix.style.height = '0'
+    document.body.appendChild(clearFix)
+  }
+
+  function setupInPageLinks() {
+    function getPagePosition() {
+      return {
+        x:
+          window.pageXOffset === undefined
+            ? document.documentElement.scrollLeft
+            : window.pageXOffset,
+        y:
+          window.pageYOffset === undefined
+            ? document.documentElement.scrollTop
+            : window.pageYOffset
+      }
+    }
+
+    function getElementPosition(el) {
+      var elPosition = el.getBoundingClientRect(),
+        pagePosition = getPagePosition()
+
+      return {
+        x: parseInt(elPosition.left, 10) + parseInt(pagePosition.x, 10),
+        y: parseInt(elPosition.top, 10) + parseInt(pagePosition.y, 10)
+      }
+    }
+
+    function findTarget(location) {
+      function jumpToTarget(target) {
+        var jumpPosition = getElementPosition(target)
+
+        log(
+          'Moving to in page link (#' +
+            hash +
+            ') at x: ' +
+            jumpPosition.x +
+            ' y: ' +
+            jumpPosition.y
+        )
+        sendMsg(jumpPosition.y, jumpPosition.x, 'scrollToOffset') // X&Y reversed at sendMsg uses height/width
+      }
+
+      var hash = location.split('#')[1] || location, // Remove # if present
+        hashData = decodeURIComponent(hash),
+        target =
+          document.getElementById(hashData) ||
+          document.getElementsByName(hashData)[0]
+
+      if (undefined === target) {
+        log(
+          'In page link (#' +
+            hash +
+            ') not found in iFrame, so sending to parent'
+        )
+        sendMsg(0, 0, 'inPageLink', '#' + hash)
+      } else {
+        jumpToTarget(target)
+      }
+    }
+
+    function checkLocationHash() {
+      var hash = window.location.hash
+      var href = window.location.href
+
+      if ('' !== hash && '#' !== hash) {
+        findTarget(href)
+      }
+    }
+
+    function bindAnchors() {
+      function setupLink(el) {
+        function linkClicked(e) {
+          e.preventDefault()
+
+          /* jshint validthis:true */
+          findTarget(this.getAttribute('href'))
+        }
+
+        if ('#' !== el.getAttribute('href')) {
+          addEventListener(el, 'click', linkClicked)
+        }
+      }
+
+      Array.prototype.forEach.call(
+        document.querySelectorAll('a[href^="#"]'),
+        setupLink
+      )
+    }
+
+    function bindLocationHash() {
+      addEventListener(window, 'hashchange', checkLocationHash)
+    }
+
+    function initCheck() {
+      // Check if page loaded with location hash after init resize
+      setTimeout(checkLocationHash, eventCancelTimer)
+    }
+
+    function enableInPageLinks() {
+      /* istanbul ignore else */ // Not testable in phantonJS
+      if (Array.prototype.forEach && document.querySelectorAll) {
+        log('Setting up location.hash handlers')
+        bindAnchors()
+        bindLocationHash()
+        initCheck()
+      } else {
+        warn(
+          'In page linking not fully supported in this browser! (See README.md for IE8 workaround)'
+        )
+      }
+    }
+
+    if (inPageLinks.enable) {
+      enableInPageLinks()
+    } else {
+      log('In page linking not enabled')
+    }
+
+    return {
+      findTarget: findTarget
+    }
+  }
+
+  function setupMouseEvents() {
+    if (mouseEvents !== true) return
+
+    function sendMouse(e) {
+      sendMsg(0, 0, e.type, e.screenY + ':' + e.screenX)
+    }
+
+    function addMouseListener(evt, name) {
+      log('Add event listener: ' + name)
+      addEventListener(window.document, evt, sendMouse)
+    }
+
+    addMouseListener('mouseenter', 'Mouse Enter')
+    addMouseListener('mouseleave', 'Mouse Leave')
+  }
+
+  function setupPublicMethods() {
+    log('Enable public methods')
+
+    win.parentIFrame = {
+      autoResize: function autoResizeF(resize) {
+        if (true === resize && false === autoResize) {
+          autoResize = true
+          startEventListeners()
+        } else if (false === resize && true === autoResize) {
+          autoResize = false
+          stopEventListeners()
+        }
+        sendMsg(0, 0, 'autoResize', JSON.stringify(autoResize))
+        return autoResize
+      },
+
+      close: function closeF() {
+        sendMsg(0, 0, 'close')
+        // teardown()
+      },
+
+      getId: function getIdF() {
+        return myID
+      },
+
+      getPageInfo: function getPageInfoF(callback) {
+        if ('function' === typeof callback) {
+          onPageInfo = callback
+          sendMsg(0, 0, 'pageInfo')
+        } else {
+          onPageInfo = function () {}
+          sendMsg(0, 0, 'pageInfoStop')
+        }
+      },
+
+      moveToAnchor: function moveToAnchorF(hash) {
+        inPageLinks.findTarget(hash)
+      },
+
+      reset: function resetF() {
+        resetIFrame('parentIFrame.reset')
+      },
+
+      scrollTo: function scrollToF(x, y) {
+        sendMsg(y, x, 'scrollTo') // X&Y reversed at sendMsg uses height/width
+      },
+
+      scrollToOffset: function scrollToF(x, y) {
+        sendMsg(y, x, 'scrollToOffset') // X&Y reversed at sendMsg uses height/width
+      },
+
+      sendMessage: function sendMessageF(msg, targetOrigin) {
+        sendMsg(0, 0, 'message', JSON.stringify(msg), targetOrigin)
+      },
+
+      setHeightCalculationMethod: function setHeightCalculationMethodF(
+        heightCalculationMethod
+      ) {
+        heightCalcMode = heightCalculationMethod
+        checkHeightMode()
+      },
+
+      setWidthCalculationMethod: function setWidthCalculationMethodF(
+        widthCalculationMethod
+      ) {
+        widthCalcMode = widthCalculationMethod
+        checkWidthMode()
+      },
+
+      setTargetOrigin: function setTargetOriginF(targetOrigin) {
+        log('Set targetOrigin: ' + targetOrigin)
+        targetOriginDefault = targetOrigin
+      },
+
+      size: function sizeF(customHeight, customWidth) {
+        var valString =
+          '' + (customHeight || '') + (customWidth ? ',' + customWidth : '')
+        sendSize(
+          'size',
+          'parentIFrame.size(' + valString + ')',
+          customHeight,
+          customWidth
+        )
+      }
+    }
+  }
+
+  function initInterval() {
+    if (0 !== interval) {
+      log('setInterval: ' + interval + 'ms')
+      intervalTimer = setInterval(function () {
+        sendSize('interval', 'setInterval: ' + interval)
+      }, Math.abs(interval))
+    }
+  }
+
+  // Not testable in PhantomJS
+  /* istanbul ignore next */
+  function setupBodyMutationObserver() {
+    function addImageLoadListners(mutation) {
+      function addImageLoadListener(element) {
+        if (false === element.complete) {
+          log('Attach listeners to ' + element.src)
+          element.addEventListener('load', imageLoaded, false)
+          element.addEventListener('error', imageError, false)
+          elements.push(element)
+        }
+      }
+
+      if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
+        addImageLoadListener(mutation.target)
+      } else if (mutation.type === 'childList') {
+        Array.prototype.forEach.call(
+          mutation.target.querySelectorAll('img'),
+          addImageLoadListener
+        )
+      }
+    }
+
+    function removeFromArray(element) {
+      elements.splice(elements.indexOf(element), 1)
+    }
+
+    function removeImageLoadListener(element) {
+      log('Remove listeners from ' + element.src)
+      element.removeEventListener('load', imageLoaded, false)
+      element.removeEventListener('error', imageError, false)
+      removeFromArray(element)
+    }
+
+    function imageEventTriggered(event, type, typeDesc) {
+      removeImageLoadListener(event.target)
+      sendSize(type, typeDesc + ': ' + event.target.src)
+    }
+
+    function imageLoaded(event) {
+      imageEventTriggered(event, 'imageLoad', 'Image loaded')
+    }
+
+    function imageError(event) {
+      imageEventTriggered(event, 'imageLoadFailed', 'Image load failed')
+    }
+
+    function mutationObserved(mutations) {
+      sendSize(
+        'mutationObserver',
+        'mutationObserver: ' + mutations[0].target + ' ' + mutations[0].type
+      )
+
+      // Deal with WebKit / Blink asyncing image loading when tags are injected into the page
+      mutations.forEach(addImageLoadListners)
+    }
+
+    function createMutationObserver() {
+      var target = document.querySelector('body'),
+        config = {
+          attributes: true,
+          attributeOldValue: false,
+          characterData: true,
+          characterDataOldValue: false,
+          childList: true,
+          subtree: true
+        }
+
+      observer = new MutationObserver(mutationObserved)
+
+      log('Create body MutationObserver')
+      observer.observe(target, config)
+
+      return observer
+    }
+
+    var elements = [],
+      MutationObserver =
+        window.MutationObserver || window.WebKitMutationObserver,
+      observer = createMutationObserver()
+
+    return {
+      disconnect: function () {
+        if ('disconnect' in observer) {
+          log('Disconnect body MutationObserver')
+          observer.disconnect()
+          elements.forEach(removeImageLoadListener)
+        }
+      }
+    }
+  }
+
+  function setupMutationObserver() {
+    var forceIntervalTimer = 0 > interval
+
+    // Not testable in PhantomJS
+    /* istanbul ignore if */ if (
+      window.MutationObserver ||
+      window.WebKitMutationObserver
+    ) {
+      if (forceIntervalTimer) {
+        initInterval()
+      } else {
+        bodyObserver = setupBodyMutationObserver()
+      }
+    } else {
+      log('MutationObserver not supported in this browser!')
+      initInterval()
+    }
+  }
+
+  // document.documentElement.offsetHeight is not reliable, so
+  // we have to jump through hoops to get a better value.
+  function getComputedStyle(prop, el) {
+    var retVal = 0
+    el = el || document.body // Not testable in phantonJS
+
+    retVal = document.defaultView.getComputedStyle(el, null)
+    retVal = null === retVal ? 0 : retVal[prop]
+
+    return parseInt(retVal, base)
+  }
+
+  function chkEventThottle(timer) {
+    if (timer > throttledTimer / 2) {
+      throttledTimer = 2 * timer
+      log('Event throttle increased to ' + throttledTimer + 'ms')
+    }
+  }
+
+  // Idea from https://github.com/guardian/iframe-messenger
+  function getMaxElement(side, elements) {
+    var elementsLength = elements.length,
+      elVal = 0,
+      maxVal = 0,
+      Side = capitalizeFirstLetter(side),
+      timer = Date.now()
+
+    for (var i = 0; i < elementsLength; i++) {
+      elVal =
+        elements[i].getBoundingClientRect()[side] +
+        getComputedStyle('margin' + Side, elements[i])
+      if (elVal > maxVal) {
+        maxVal = elVal
+      }
+    }
+
+    timer = Date.now() - timer
+
+    log('Parsed ' + elementsLength + ' HTML elements')
+    log('Element position calculated in ' + timer + 'ms')
+
+    chkEventThottle(timer)
+
+    return maxVal
+  }
+
+  function getAllMeasurements(dimensions) {
+    return [
+      dimensions.bodyOffset(),
+      dimensions.bodyScroll(),
+      dimensions.documentElementOffset(),
+      dimensions.documentElementScroll()
+    ]
+  }
+
+  function getTaggedElements(side, tag) {
+    function noTaggedElementsFound() {
+      warn('No tagged elements (' + tag + ') found on page')
+      return document.querySelectorAll('body *')
+    }
+
+    var elements = document.querySelectorAll('[' + tag + ']')
+
+    if (elements.length === 0) noTaggedElementsFound()
+
+    return getMaxElement(side, elements)
+  }
+
+  function getAllElements() {
+    return document.querySelectorAll('body *')
+  }
+
+  var getHeight = {
+      bodyOffset: function getBodyOffsetHeight() {
+        return (
+          document.body.offsetHeight +
+          getComputedStyle('marginTop') +
+          getComputedStyle('marginBottom')
+        )
+      },
+
+      offset: function () {
+        return getHeight.bodyOffset() // Backwards compatibility
+      },
+
+      bodyScroll: function getBodyScrollHeight() {
+        return document.body.scrollHeight
+      },
+
+      custom: function getCustomWidth() {
+        return customCalcMethods.height()
+      },
+
+      documentElementOffset: function getDEOffsetHeight() {
+        return document.documentElement.offsetHeight
+      },
+
+      documentElementScroll: function getDEScrollHeight() {
+        return document.documentElement.scrollHeight
+      },
+
+      max: function getMaxHeight() {
+        return Math.max.apply(null, getAllMeasurements(getHeight))
+      },
+
+      min: function getMinHeight() {
+        return Math.min.apply(null, getAllMeasurements(getHeight))
+      },
+
+      grow: function growHeight() {
+        return getHeight.max() // Run max without the forced downsizing
+      },
+
+      lowestElement: function getBestHeight() {
+        return Math.max(
+          getHeight.bodyOffset() || getHeight.documentElementOffset(),
+          getMaxElement('bottom', getAllElements())
+        )
+      },
+
+      taggedElement: function getTaggedElementsHeight() {
+        return getTaggedElements('bottom', 'data-iframe-height')
+      }
+    },
+    getWidth = {
+      bodyScroll: function getBodyScrollWidth() {
+        return document.body.scrollWidth
+      },
+
+      bodyOffset: function getBodyOffsetWidth() {
+        return document.body.offsetWidth
+      },
+
+      custom: function getCustomWidth() {
+        return customCalcMethods.width()
+      },
+
+      documentElementScroll: function getDEScrollWidth() {
+        return document.documentElement.scrollWidth
+      },
+
+      documentElementOffset: function getDEOffsetWidth() {
+        return document.documentElement.offsetWidth
+      },
+
+      scroll: function getMaxWidth() {
+        return Math.max(getWidth.bodyScroll(), getWidth.documentElementScroll())
+      },
+
+      max: function getMaxWidth() {
+        return Math.max.apply(null, getAllMeasurements(getWidth))
+      },
+
+      min: function getMinWidth() {
+        return Math.min.apply(null, getAllMeasurements(getWidth))
+      },
+
+      rightMostElement: function rightMostElement() {
+        return getMaxElement('right', getAllElements())
+      },
+
+      taggedElement: function getTaggedElementsWidth() {
+        return getTaggedElements('right', 'data-iframe-width')
+      }
+    }
+
+  function sizeIFrame(
+    triggerEvent,
+    triggerEventDesc,
+    customHeight,
+    customWidth
+  ) {
+    function resizeIFrame() {
+      height = currentHeight
+      width = currentWidth
+
+      sendMsg(height, width, triggerEvent)
+    }
+
+    function isSizeChangeDetected() {
+      function checkTolarance(a, b) {
+        var retVal = Math.abs(a - b) <= tolerance
+        return !retVal
+      }
+
+      currentHeight =
+        undefined === customHeight ? getHeight[heightCalcMode]() : customHeight
+      currentWidth =
+        undefined === customWidth ? getWidth[widthCalcMode]() : customWidth
+
+      return (
+        checkTolarance(height, currentHeight) ||
+        (calculateWidth && checkTolarance(width, currentWidth))
+      )
+    }
+
+    function isForceResizableEvent() {
+      return !(triggerEvent in { init: 1, interval: 1, size: 1 })
+    }
+
+    function isForceResizableCalcMode() {
+      return (
+        heightCalcMode in resetRequiredMethods ||
+        (calculateWidth && widthCalcMode in resetRequiredMethods)
+      )
+    }
+
+    function logIgnored() {
+      log('No change in size detected')
+    }
+
+    function checkDownSizing() {
+      if (isForceResizableEvent() && isForceResizableCalcMode()) {
+        resetIFrame(triggerEventDesc)
+      } else if (!(triggerEvent in { interval: 1 })) {
+        logIgnored()
+      }
+    }
+
+    var currentHeight, currentWidth
+
+    if (isSizeChangeDetected() || 'init' === triggerEvent) {
+      lockTrigger()
+      resizeIFrame()
+    } else {
+      checkDownSizing()
+    }
+  }
+
+  var sizeIFrameThrottled = throttle(sizeIFrame)
+
+  function sendSize(triggerEvent, triggerEventDesc, customHeight, customWidth) {
+    function recordTrigger() {
+      if (!(triggerEvent in { reset: 1, resetPage: 1, init: 1 })) {
+        log('Trigger event: ' + triggerEventDesc)
+      }
+    }
+
+    function isDoubleFiredEvent() {
+      return triggerLocked && triggerEvent in doubleEventList
+    }
+
+    if (isDoubleFiredEvent()) {
+      log('Trigger event cancelled: ' + triggerEvent)
+    } else {
+      recordTrigger()
+      if (triggerEvent === 'init') {
+        sizeIFrame(triggerEvent, triggerEventDesc, customHeight, customWidth)
+      } else {
+        sizeIFrameThrottled(
+          triggerEvent,
+          triggerEventDesc,
+          customHeight,
+          customWidth
+        )
+      }
+    }
+  }
+
+  function lockTrigger() {
+    if (!triggerLocked) {
+      triggerLocked = true
+      log('Trigger event lock on')
+    }
+    clearTimeout(triggerLockedTimer)
+    triggerLockedTimer = setTimeout(function () {
+      triggerLocked = false
+      log('Trigger event lock off')
+      log('--')
+    }, eventCancelTimer)
+  }
+
+  function triggerReset(triggerEvent) {
+    height = getHeight[heightCalcMode]()
+    width = getWidth[widthCalcMode]()
+
+    sendMsg(height, width, triggerEvent)
+  }
+
+  function resetIFrame(triggerEventDesc) {
+    var hcm = heightCalcMode
+    heightCalcMode = heightCalcModeDefault
+
+    log('Reset trigger event: ' + triggerEventDesc)
+    lockTrigger()
+    triggerReset('reset')
+
+    heightCalcMode = hcm
+  }
+
+  function sendMsg(height, width, triggerEvent, msg, targetOrigin) {
+    function setTargetOrigin() {
+      if (undefined === targetOrigin) {
+        targetOrigin = targetOriginDefault
+      } else {
+        log('Message targetOrigin: ' + targetOrigin)
+      }
+    }
+
+    function sendToParent() {
+      var size = height + ':' + width,
+        message =
+          myID +
+          ':' +
+          size +
+          ':' +
+          triggerEvent +
+          (undefined === msg ? '' : ':' + msg)
+
+      log('Sending message to host page (' + message + ')')
+      target.postMessage(msgID + message, targetOrigin)
+    }
+
+    if (true === sendPermit) {
+      setTargetOrigin()
+      sendToParent()
+    }
+  }
+
+  function receiver(event) {
+    var processRequestFromParent = {
+      init: function initFromParent() {
+        initMsg = event.data
+        target = event.source
+
+        init()
+        firstRun = false
+        setTimeout(function () {
+          initLock = false
+        }, eventCancelTimer)
+      },
+
+      reset: function resetFromParent() {
+        if (initLock) {
+          log('Page reset ignored by init')
+        } else {
+          log('Page size reset by host page')
+          triggerReset('resetPage')
+        }
+      },
+
+      resize: function resizeFromParent() {
+        sendSize('resizeParent', 'Parent window requested size check')
+      },
+
+      moveToAnchor: function moveToAnchorF() {
+        inPageLinks.findTarget(getData())
+      },
+      inPageLink: function inPageLinkF() {
+        this.moveToAnchor()
+      }, // Backward compatibility
+
+      pageInfo: function pageInfoFromParent() {
+        var msgBody = getData()
+        log('PageInfoFromParent called from parent: ' + msgBody)
+        onPageInfo(JSON.parse(msgBody))
+        log(' --')
+      },
+
+      message: function messageFromParent() {
+        var msgBody = getData()
+
+        log('onMessage called from parent: ' + msgBody)
+        // eslint-disable-next-line sonarjs/no-extra-arguments
+        onMessage(JSON.parse(msgBody))
+        log(' --')
+      }
+    }
+
+    function isMessageForUs() {
+      return msgID === ('' + event.data).slice(0, msgIdLen) // ''+ Protects against non-string messages
+    }
+
+    function getMessageType() {
+      return event.data.split(']')[1].split(':')[0]
+    }
+
+    function getData() {
+      return event.data.slice(event.data.indexOf(':') + 1)
+    }
+
+    function isMiddleTier() {
+      return (
+        (!(typeof module !== 'undefined' && module.exports) &&
+          'iFrameResize' in window) ||
+        (window.jQuery !== undefined &&
+          'iFrameResize' in window.jQuery.prototype)
+      )
+    }
+
+    function isInitMsg() {
+      // Test if this message is from a child below us. This is an ugly test, however, updating
+      // the message format would break backwards compatibility.
+      return event.data.split(':')[2] in { true: 1, false: 1 }
+    }
+
+    function callFromParent() {
+      var messageType = getMessageType()
+
+      if (messageType in processRequestFromParent) {
+        processRequestFromParent[messageType]()
+      } else if (!isMiddleTier() && !isInitMsg()) {
+        warn('Unexpected message (' + event.data + ')')
+      }
+    }
+
+    function processMessage() {
+      if (false === firstRun) {
+        callFromParent()
+      } else if (isInitMsg()) {
+        processRequestFromParent.init()
+      } else {
+        log(
+          'Ignored message of type "' +
+            getMessageType() +
+            '". Received before initialization.'
+        )
+      }
+    }
+
+    if (isMessageForUs()) {
+      processMessage()
+    }
+  }
+
+  // Normally the parent kicks things off when it detects the iFrame has loaded.
+  // If this script is async-loaded, then tell parent page to retry init.
+  function chkLateLoaded() {
+    if ('loading' !== document.readyState) {
+      window.parent.postMessage('[iFrameResizerChild]Ready', '*')
+    }
+  }
+
+  addEventListener(window, 'message', receiver)
+  addEventListener(window, 'readystatechange', chkLateLoaded)
+  chkLateLoaded()
+
+  
+})()
diff --git a/apps/routerconsole/jsp/js/iframeResizer.js b/apps/routerconsole/jsp/js/iframeResizer.js
new file mode 100644
index 0000000000000000000000000000000000000000..4ea1d309a94a9ca1bafe0a5dbcf713f0496167c8
--- /dev/null
+++ b/apps/routerconsole/jsp/js/iframeResizer.js
@@ -0,0 +1,1466 @@
+/*
+ * File: iframeResizer.js
+ * Desc: Force iframes to size to content.
+ * Requires: iframeResizer.contentWindow.js to be loaded into the target frame.
+ * Doc: https://github.com/davidjbradshaw/iframe-resizer
+ * Author: David J. Bradshaw - dave@bradshaw.net
+ * Contributor: Jure Mav - jure.mav@gmail.com
+ * Contributor: Reed Dadoune - reed@dadoune.com
+ */
+
+// eslint-disable-next-line sonarjs/cognitive-complexity, no-shadow-restricted-names
+;(function (undefined) {
+  if (typeof window === 'undefined') return // don't run for server side render
+
+  var count = 0,
+    logEnabled = false,
+    hiddenCheckEnabled = false,
+    msgHeader = 'message',
+    msgHeaderLen = msgHeader.length,
+    msgId = '[iFrameSizer]', // Must match iframe msg ID
+    msgIdLen = msgId.length,
+    pagePosition = null,
+    requestAnimationFrame = window.requestAnimationFrame,
+    resetRequiredMethods = Object.freeze({
+      max: 1,
+      scroll: 1,
+      bodyScroll: 1,
+      documentElementScroll: 1
+    }),
+    settings = {},
+    timer = null,
+    defaults = Object.freeze({
+      autoResize: true,
+      bodyBackground: null,
+      bodyMargin: null,
+      bodyMarginV1: 8,
+      bodyPadding: null,
+      checkOrigin: true,
+      inPageLinks: false,
+      enablePublicMethods: true,
+      heightCalculationMethod: 'bodyOffset',
+      id: 'iFrameResizer',
+      interval: 32,
+      log: false,
+      maxHeight: Infinity,
+      maxWidth: Infinity,
+      minHeight: 0,
+      minWidth: 0,
+      mouseEvents: true,
+      resizeFrom: 'parent',
+      scrolling: false,
+      sizeHeight: true,
+      sizeWidth: false,
+      warningTimeout: 5000,
+      tolerance: 0,
+      widthCalculationMethod: 'scroll',
+      onClose: function () {
+        return true
+      },
+      onClosed: function () {},
+      onInit: function () {},
+      onMessage: function () {
+        warn('onMessage function not defined')
+      },
+      onMouseEnter: function () {},
+      onMouseLeave: function () {},
+      onResized: function () {},
+      onScroll: function () {
+        return true
+      }
+    })
+
+  function getMutationObserver() {
+    return (
+      window.MutationObserver ||
+      window.WebKitMutationObserver ||
+      window.MozMutationObserver
+    )
+  }
+
+  function addEventListener(el, evt, func) {
+    el.addEventListener(evt, func, false)
+  }
+
+  function removeEventListener(el, evt, func) {
+    el.removeEventListener(evt, func, false)
+  }
+
+  function setupRequestAnimationFrame() {
+    var vendors = ['moz', 'webkit', 'o', 'ms']
+    var x
+
+    // Remove vendor prefixing if prefixed and break early if not
+    for (x = 0; x < vendors.length && !requestAnimationFrame; x += 1) {
+      requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']
+    }
+
+    if (requestAnimationFrame) {
+      // Firefox extension content-scripts have a globalThis object that is not the same as window.
+      // Binding `requestAnimationFrame` to window allows the function to work and prevents errors
+      // being thrown when run in that context, and should be a no-op in every other context.
+      requestAnimationFrame = requestAnimationFrame.bind(window)
+    } else {
+      log('setup', 'RequestAnimationFrame not supported')
+    }
+  }
+
+  function getMyID(iframeId) {
+    var retStr = 'Host page: ' + iframeId
+
+    if (window.top !== window.self) {
+      retStr =
+        window.parentIFrame && window.parentIFrame.getId
+          ? window.parentIFrame.getId() + ': ' + iframeId
+          : 'Nested host page: ' + iframeId
+    }
+
+    return retStr
+  }
+
+  function formatLogHeader(iframeId) {
+    return msgId + '[' + getMyID(iframeId) + ']'
+  }
+
+  function isLogEnabled(iframeId) {
+    return settings[iframeId] ? settings[iframeId].log : logEnabled
+  }
+
+  function log(iframeId, msg) {
+    output('log', iframeId, msg, isLogEnabled(iframeId))
+  }
+
+  function info(iframeId, msg) {
+    output('info', iframeId, msg, isLogEnabled(iframeId))
+  }
+
+  function warn(iframeId, msg) {
+    output('warn', iframeId, msg, true)
+  }
+
+  function output(type, iframeId, msg, enabled) {
+    if (true === enabled && 'object' === typeof window.console) {
+      // eslint-disable-next-line no-console
+      console[type](formatLogHeader(iframeId), msg)
+    }
+  }
+
+  function iFrameListener(event) {
+    function resizeIFrame() {
+      function resize() {
+        setSize(messageData)
+        setPagePosition(iframeId)
+        on('onResized', messageData)
+      }
+
+      ensureInRange('Height')
+      ensureInRange('Width')
+
+      syncResize(resize, messageData, 'init')
+    }
+
+    function processMsg() {
+      var data = msg.slice(msgIdLen).split(':')
+      var height = data[1] ? parseInt(data[1], 10) : 0
+      var iframe = settings[data[0]] && settings[data[0]].iframe
+      var compStyle = getComputedStyle(iframe)
+
+      return {
+        iframe: iframe,
+        id: data[0],
+        height: height + getPaddingEnds(compStyle) + getBorderEnds(compStyle),
+        width: data[2],
+        type: data[3]
+      }
+    }
+
+    function getPaddingEnds(compStyle) {
+      if (compStyle.boxSizing !== 'border-box') {
+        return 0
+      }
+      var top = compStyle.paddingTop ? parseInt(compStyle.paddingTop, 10) : 0
+      var bot = compStyle.paddingBottom
+        ? parseInt(compStyle.paddingBottom, 10)
+        : 0
+      return top + bot
+    }
+
+    function getBorderEnds(compStyle) {
+      if (compStyle.boxSizing !== 'border-box') {
+        return 0
+      }
+      var top = compStyle.borderTopWidth
+        ? parseInt(compStyle.borderTopWidth, 10)
+        : 0
+      var bot = compStyle.borderBottomWidth
+        ? parseInt(compStyle.borderBottomWidth, 10)
+        : 0
+      return top + bot
+    }
+
+    function ensureInRange(Dimension) {
+      var max = Number(settings[iframeId]['max' + Dimension]),
+        min = Number(settings[iframeId]['min' + Dimension]),
+        dimension = Dimension.toLowerCase(),
+        size = Number(messageData[dimension])
+
+      log(iframeId, 'Checking ' + dimension + ' is in range ' + min + '-' + max)
+
+      if (size < min) {
+        size = min
+        log(iframeId, 'Set ' + dimension + ' to min value')
+      }
+
+      if (size > max) {
+        size = max
+        log(iframeId, 'Set ' + dimension + ' to max value')
+      }
+
+      messageData[dimension] = '' + size
+    }
+
+    function isMessageFromIFrame() {
+      function checkAllowedOrigin() {
+        function checkList() {
+          var i = 0,
+            retCode = false
+
+          log(
+            iframeId,
+            'Checking connection is from allowed list of origins: ' +
+              checkOrigin
+          )
+
+          for (; i < checkOrigin.length; i++) {
+            if (checkOrigin[i] === origin) {
+              retCode = true
+              break
+            }
+          }
+          return retCode
+        }
+
+        function checkSingle() {
+          var remoteHost = settings[iframeId] && settings[iframeId].remoteHost
+          log(iframeId, 'Checking connection is from: ' + remoteHost)
+          return origin === remoteHost
+        }
+
+        return checkOrigin.constructor === Array ? checkList() : checkSingle()
+      }
+
+      var origin = event.origin,
+        checkOrigin = settings[iframeId] && settings[iframeId].checkOrigin
+
+      if (checkOrigin && '' + origin !== 'null' && !checkAllowedOrigin()) {
+        throw new Error(
+          'Unexpected message received from: ' +
+            origin +
+            ' for ' +
+            messageData.iframe.id +
+            '. Message was: ' +
+            event.data +
+            '. This error can be disabled by setting the checkOrigin: false option or by providing of array of trusted domains.'
+        )
+      }
+
+      return true
+    }
+
+    function isMessageForUs() {
+      return (
+        msgId === ('' + msg).slice(0, msgIdLen) &&
+        msg.slice(msgIdLen).split(':')[0] in settings
+      ) // ''+Protects against non-string msg
+    }
+
+    function isMessageFromMetaParent() {
+      // Test if this message is from a parent above us. This is an ugly test, however, updating
+      // the message format would break backwards compatibility.
+      var retCode = messageData.type in { true: 1, false: 1, undefined: 1 }
+
+      if (retCode) {
+        log(iframeId, 'Ignoring init message from meta parent page')
+      }
+
+      return retCode
+    }
+
+    function getMsgBody(offset) {
+      return msg.slice(msg.indexOf(':') + msgHeaderLen + offset)
+    }
+
+    function forwardMsgFromIFrame(msgBody) {
+      log(
+        iframeId,
+        'onMessage passed: {iframe: ' +
+          messageData.iframe.id +
+          ', message: ' +
+          msgBody +
+          '}'
+      )
+
+      on('onMessage', {
+        iframe: messageData.iframe,
+        message: JSON.parse(msgBody)
+      })
+
+      log(iframeId, '--')
+    }
+
+    function getPageInfo() {
+      var bodyPosition = document.body.getBoundingClientRect(),
+        iFramePosition = messageData.iframe.getBoundingClientRect()
+
+      return JSON.stringify({
+        iframeHeight: iFramePosition.height,
+        iframeWidth: iFramePosition.width,
+        clientHeight: Math.max(
+          document.documentElement.clientHeight,
+          window.innerHeight || 0
+        ),
+        clientWidth: Math.max(
+          document.documentElement.clientWidth,
+          window.innerWidth || 0
+        ),
+        offsetTop: parseInt(iFramePosition.top - bodyPosition.top, 10),
+        offsetLeft: parseInt(iFramePosition.left - bodyPosition.left, 10),
+        scrollTop: window.pageYOffset,
+        scrollLeft: window.pageXOffset,
+        documentHeight: document.documentElement.clientHeight,
+        documentWidth: document.documentElement.clientWidth,
+        windowHeight: window.innerHeight,
+        windowWidth: window.innerWidth
+      })
+    }
+
+    function sendPageInfoToIframe(iframe, iframeId) {
+      function debouncedTrigger() {
+        trigger('Send Page Info', 'pageInfo:' + getPageInfo(), iframe, iframeId)
+      }
+      debounceFrameEvents(debouncedTrigger, 32, iframeId)
+    }
+
+    function startPageInfoMonitor() {
+      function setListener(type, func) {
+        function sendPageInfo() {
+          if (settings[id]) {
+            sendPageInfoToIframe(settings[id].iframe, id)
+          } else {
+            stop()
+          }
+        }
+
+        ;['scroll', 'resize'].forEach(function (evt) {
+          log(id, type + evt + ' listener for sendPageInfo')
+          func(window, evt, sendPageInfo)
+        })
+      }
+
+      function stop() {
+        setListener('Remove ', removeEventListener)
+      }
+
+      function start() {
+        setListener('Add ', addEventListener)
+      }
+
+      var id = iframeId // Create locally scoped copy of iFrame ID
+
+      start()
+
+      if (settings[id]) {
+        settings[id].stopPageInfo = stop
+      }
+    }
+
+    function stopPageInfoMonitor() {
+      if (settings[iframeId] && settings[iframeId].stopPageInfo) {
+        settings[iframeId].stopPageInfo()
+        delete settings[iframeId].stopPageInfo
+      }
+    }
+
+    function checkIFrameExists() {
+      var retBool = true
+
+      if (null === messageData.iframe) {
+        warn(iframeId, 'IFrame (' + messageData.id + ') not found')
+        retBool = false
+      }
+      return retBool
+    }
+
+    function getElementPosition(target) {
+      var iFramePosition = target.getBoundingClientRect()
+
+      getPagePosition(iframeId)
+
+      return {
+        x: Math.floor(Number(iFramePosition.left) + Number(pagePosition.x)),
+        y: Math.floor(Number(iFramePosition.top) + Number(pagePosition.y))
+      }
+    }
+
+    function scrollRequestFromChild(addOffset) {
+      /* istanbul ignore next */ // Not testable in Karma
+      function reposition() {
+        pagePosition = newPosition
+        scrollTo()
+        log(iframeId, '--')
+      }
+
+      function calcOffset() {
+        return {
+          x: Number(messageData.width) + offset.x,
+          y: Number(messageData.height) + offset.y
+        }
+      }
+
+      function scrollParent() {
+        if (window.parentIFrame) {
+          window.parentIFrame['scrollTo' + (addOffset ? 'Offset' : '')](
+            newPosition.x,
+            newPosition.y
+          )
+        } else {
+          warn(
+            iframeId,
+            'Unable to scroll to requested position, window.parentIFrame not found'
+          )
+        }
+      }
+
+      var offset = addOffset
+          ? getElementPosition(messageData.iframe)
+          : { x: 0, y: 0 },
+        newPosition = calcOffset()
+
+      log(
+        iframeId,
+        'Reposition requested from iFrame (offset x:' +
+          offset.x +
+          ' y:' +
+          offset.y +
+          ')'
+      )
+
+      if (window.top === window.self) {
+        reposition()
+      } else {
+        scrollParent()
+      }
+    }
+
+    function scrollTo() {
+      if (false === on('onScroll', pagePosition)) {
+        unsetPagePosition()
+      } else {
+        setPagePosition(iframeId)
+      }
+    }
+
+    function findTarget(location) {
+      function jumpToTarget() {
+        var jumpPosition = getElementPosition(target)
+
+        log(
+          iframeId,
+          'Moving to in page link (#' +
+            hash +
+            ') at x: ' +
+            jumpPosition.x +
+            ' y: ' +
+            jumpPosition.y
+        )
+        pagePosition = {
+          x: jumpPosition.x,
+          y: jumpPosition.y
+        }
+
+        scrollTo()
+        log(iframeId, '--')
+      }
+
+      function jumpToParent() {
+        if (window.parentIFrame) {
+          window.parentIFrame.moveToAnchor(hash)
+        } else {
+          log(
+            iframeId,
+            'In page link #' +
+              hash +
+              ' not found and window.parentIFrame not found'
+          )
+        }
+      }
+
+      var hash = location.split('#')[1] || '',
+        hashData = decodeURIComponent(hash),
+        target =
+          document.getElementById(hashData) ||
+          document.getElementsByName(hashData)[0]
+
+      if (target) {
+        jumpToTarget()
+      } else if (window.top === window.self) {
+        log(iframeId, 'In page link #' + hash + ' not found')
+      } else {
+        jumpToParent()
+      }
+    }
+
+    function onMouse(event) {
+      var mousePos = {}
+
+      if (Number(messageData.width) === 0 && Number(messageData.height) === 0) {
+        var data = getMsgBody(9).split(':')
+        mousePos = {
+          x: data[1],
+          y: data[0]
+        }
+      } else {
+        mousePos = {
+          x: messageData.width,
+          y: messageData.height
+        }
+      }
+
+      on(event, {
+        iframe: messageData.iframe,
+        screenX: Number(mousePos.x),
+        screenY: Number(mousePos.y),
+        type: messageData.type
+      })
+    }
+
+    function on(funcName, val) {
+      return chkEvent(iframeId, funcName, val)
+    }
+
+    function actionMsg() {
+      if (settings[iframeId] && settings[iframeId].firstRun) firstRun()
+
+      switch (messageData.type) {
+        case 'close': {
+          closeIFrame(messageData.iframe)
+          break
+        }
+
+        case 'message': {
+          forwardMsgFromIFrame(getMsgBody(6))
+          break
+        }
+
+        case 'mouseenter': {
+          onMouse('onMouseEnter')
+          break
+        }
+
+        case 'mouseleave': {
+          onMouse('onMouseLeave')
+          break
+        }
+
+        case 'autoResize': {
+          settings[iframeId].autoResize = JSON.parse(getMsgBody(9))
+          break
+        }
+
+        case 'scrollTo': {
+          scrollRequestFromChild(false)
+          break
+        }
+
+        case 'scrollToOffset': {
+          scrollRequestFromChild(true)
+          break
+        }
+
+        case 'pageInfo': {
+          sendPageInfoToIframe(
+            settings[iframeId] && settings[iframeId].iframe,
+            iframeId
+          )
+          startPageInfoMonitor()
+          break
+        }
+
+        case 'pageInfoStop': {
+          stopPageInfoMonitor()
+          break
+        }
+
+        case 'inPageLink': {
+          findTarget(getMsgBody(9))
+          break
+        }
+
+        case 'reset': {
+          resetIFrame(messageData)
+          break
+        }
+
+        case 'init': {
+          resizeIFrame()
+          on('onInit', messageData.iframe)
+          break
+        }
+
+        default: {
+          if (
+            Number(messageData.width) === 0 &&
+            Number(messageData.height) === 0
+          ) {
+            warn(
+              'Unsupported message received (' +
+                messageData.type +
+                '), this is likely due to the iframe containing a later ' +
+                'version of iframe-resizer than the parent page'
+            )
+          } else {
+            resizeIFrame()
+          }
+        }
+      }
+    }
+
+    function hasSettings(iframeId) {
+      var retBool = true
+
+      if (!settings[iframeId]) {
+        retBool = false
+        warn(
+          messageData.type +
+            ' No settings for ' +
+            iframeId +
+            '. Message was: ' +
+            msg
+        )
+      }
+
+      return retBool
+    }
+
+    function iFrameReadyMsgReceived() {
+      // eslint-disable-next-line no-restricted-syntax, guard-for-in
+      for (var iframeId in settings) {
+        trigger(
+          'iFrame requested init',
+          createOutgoingMsg(iframeId),
+          settings[iframeId].iframe,
+          iframeId
+        )
+      }
+    }
+
+    function firstRun() {
+      if (settings[iframeId]) {
+        settings[iframeId].firstRun = false
+      }
+    }
+
+    var msg = event.data,
+      messageData = {},
+      iframeId = null
+
+    if ('[iFrameResizerChild]Ready' === msg) {
+      iFrameReadyMsgReceived()
+    } else if (isMessageForUs()) {
+      messageData = processMsg()
+      iframeId = messageData.id
+      if (settings[iframeId]) {
+        settings[iframeId].loaded = true
+      }
+
+      if (!isMessageFromMetaParent() && hasSettings(iframeId)) {
+        log(iframeId, 'Received: ' + msg)
+
+        if (checkIFrameExists() && isMessageFromIFrame()) {
+          actionMsg()
+        }
+      }
+    } else {
+      info(iframeId, 'Ignored: ' + msg)
+    }
+  }
+
+  function chkEvent(iframeId, funcName, val) {
+    var func = null,
+      retVal = null
+
+    if (settings[iframeId]) {
+      func = settings[iframeId][funcName]
+
+      if ('function' === typeof func) {
+        retVal = func(val)
+      } else {
+        throw new TypeError(
+          funcName + ' on iFrame[' + iframeId + '] is not a function'
+        )
+      }
+    }
+
+    return retVal
+  }
+
+  function removeIframeListeners(iframe) {
+    var iframeId = iframe.id
+    delete settings[iframeId]
+  }
+
+  function closeIFrame(iframe) {
+    var iframeId = iframe.id
+    if (chkEvent(iframeId, 'onClose', iframeId) === false) {
+      log(iframeId, 'Close iframe cancelled by onClose event')
+      return
+    }
+    log(iframeId, 'Removing iFrame: ' + iframeId)
+
+    try {
+      // Catch race condition error with React
+      if (iframe.parentNode) {
+        iframe.parentNode.removeChild(iframe)
+      }
+    } catch (error) {
+      warn(error)
+    }
+
+    chkEvent(iframeId, 'onClosed', iframeId)
+    log(iframeId, '--')
+    removeIframeListeners(iframe)
+  }
+
+  function getPagePosition(iframeId) {
+    if (null === pagePosition) {
+      pagePosition = {
+        x:
+          window.pageXOffset === undefined
+            ? document.documentElement.scrollLeft
+            : window.pageXOffset,
+        y:
+          window.pageYOffset === undefined
+            ? document.documentElement.scrollTop
+            : window.pageYOffset
+      }
+      log(
+        iframeId,
+        'Get page position: ' + pagePosition.x + ',' + pagePosition.y
+      )
+    }
+  }
+
+  function setPagePosition(iframeId) {
+    if (null !== pagePosition) {
+      window.scrollTo(pagePosition.x, pagePosition.y)
+      log(
+        iframeId,
+        'Set page position: ' + pagePosition.x + ',' + pagePosition.y
+      )
+      unsetPagePosition()
+    }
+  }
+
+  function unsetPagePosition() {
+    pagePosition = null
+  }
+
+  function resetIFrame(messageData) {
+    function reset() {
+      setSize(messageData)
+      trigger('reset', 'reset', messageData.iframe, messageData.id)
+    }
+
+    log(
+      messageData.id,
+      'Size reset requested by ' +
+        ('init' === messageData.type ? 'host page' : 'iFrame')
+    )
+    getPagePosition(messageData.id)
+    syncResize(reset, messageData, 'reset')
+  }
+
+  function setSize(messageData) {
+    function setDimension(dimension) {
+      if (!messageData.id) {
+        log('undefined', 'messageData id not set')
+        return
+      }
+      messageData.iframe.style[dimension] = messageData[dimension] + 'px'
+      log(
+        messageData.id,
+        'IFrame (' +
+          iframeId +
+          ') ' +
+          dimension +
+          ' set to ' +
+          messageData[dimension] +
+          'px'
+      )
+    }
+
+    function chkZero(dimension) {
+      // FireFox sets dimension of hidden iFrames to zero.
+      // So if we detect that set up an event to check for
+      // when iFrame becomes visible.
+
+      /* istanbul ignore next */ // Not testable in PhantomJS
+      if (!hiddenCheckEnabled && '0' === messageData[dimension]) {
+        hiddenCheckEnabled = true
+        log(iframeId, 'Hidden iFrame detected, creating visibility listener')
+        fixHiddenIFrames()
+      }
+    }
+
+    function processDimension(dimension) {
+      setDimension(dimension)
+      chkZero(dimension)
+    }
+
+    var iframeId = messageData.iframe.id
+
+    if (settings[iframeId]) {
+      if (settings[iframeId].sizeHeight) {
+        processDimension('height')
+      }
+      if (settings[iframeId].sizeWidth) {
+        processDimension('width')
+      }
+    }
+  }
+
+  function syncResize(func, messageData, doNotSync) {
+    /* istanbul ignore if */ // Not testable in PhantomJS
+    if (
+      doNotSync !== messageData.type &&
+      requestAnimationFrame &&
+      // including check for jasmine because had trouble getting spy to work in unit test using requestAnimationFrame
+      !window.jasmine
+    ) {
+      log(messageData.id, 'Requesting animation frame')
+      requestAnimationFrame(func)
+    } else {
+      func()
+    }
+  }
+
+  function trigger(calleeMsg, msg, iframe, id, noResponseWarning) {
+    function postMessageToIFrame() {
+      var target = settings[id] && settings[id].targetOrigin
+      log(
+        id,
+        '[' +
+          calleeMsg +
+          '] Sending msg to iframe[' +
+          id +
+          '] (' +
+          msg +
+          ') targetOrigin: ' +
+          target
+      )
+      iframe.contentWindow.postMessage(msgId + msg, target)
+    }
+
+    function iFrameNotFound() {
+      warn(id, '[' + calleeMsg + '] IFrame(' + id + ') not found')
+    }
+
+    function chkAndSend() {
+      if (
+        iframe &&
+        'contentWindow' in iframe &&
+        null !== iframe.contentWindow
+      ) {
+        // Null test for PhantomJS
+        postMessageToIFrame()
+      } else {
+        iFrameNotFound()
+      }
+    }
+
+    function warnOnNoResponse() {
+      function warning() {
+        if (settings[id] && !settings[id].loaded && !errorShown) {
+          errorShown = true
+          warn(
+            id,
+            'IFrame has not responded within ' +
+              settings[id].warningTimeout / 1000 +
+              ' seconds. Check iFrameResizer.contentWindow.js has been loaded in iFrame. This message can be ignored if everything is working, or you can set the warningTimeout option to a higher value or zero to suppress this warning.'
+          )
+        }
+      }
+
+      if (
+        !!noResponseWarning &&
+        settings[id] &&
+        !!settings[id].warningTimeout
+      ) {
+        settings[id].msgTimeout = setTimeout(
+          warning,
+          settings[id].warningTimeout
+        )
+      }
+    }
+
+    var errorShown = false
+
+    id = id || iframe.id
+
+    if (settings[id]) {
+      chkAndSend()
+      warnOnNoResponse()
+    }
+  }
+
+  function createOutgoingMsg(iframeId) {
+    return (
+      iframeId +
+      ':' +
+      settings[iframeId].bodyMarginV1 +
+      ':' +
+      settings[iframeId].sizeWidth +
+      ':' +
+      settings[iframeId].log +
+      ':' +
+      settings[iframeId].interval +
+      ':' +
+      settings[iframeId].enablePublicMethods +
+      ':' +
+      settings[iframeId].autoResize +
+      ':' +
+      settings[iframeId].bodyMargin +
+      ':' +
+      settings[iframeId].heightCalculationMethod +
+      ':' +
+      settings[iframeId].bodyBackground +
+      ':' +
+      settings[iframeId].bodyPadding +
+      ':' +
+      settings[iframeId].tolerance +
+      ':' +
+      settings[iframeId].inPageLinks +
+      ':' +
+      settings[iframeId].resizeFrom +
+      ':' +
+      settings[iframeId].widthCalculationMethod +
+      ':' +
+      settings[iframeId].mouseEvents
+    )
+  }
+
+  function isNumber(value) {
+    return typeof value === 'number'
+  }
+
+  function setupIFrame(iframe, options) {
+    function setLimits() {
+      function addStyle(style) {
+        var styleValue = settings[iframeId][style]
+        if (Infinity !== styleValue && 0 !== styleValue) {
+          iframe.style[style] = isNumber(styleValue)
+            ? styleValue + 'px'
+            : styleValue
+          log(iframeId, 'Set ' + style + ' = ' + iframe.style[style])
+        }
+      }
+
+      function chkMinMax(dimension) {
+        if (
+          settings[iframeId]['min' + dimension] >
+          settings[iframeId]['max' + dimension]
+        ) {
+          throw new Error(
+            'Value for min' +
+              dimension +
+              ' can not be greater than max' +
+              dimension
+          )
+        }
+      }
+
+      chkMinMax('Height')
+      chkMinMax('Width')
+
+      addStyle('maxHeight')
+      addStyle('minHeight')
+      addStyle('maxWidth')
+      addStyle('minWidth')
+    }
+
+    function newId() {
+      var id = (options && options.id) || defaults.id + count++
+      if (null !== document.getElementById(id)) {
+        id += count++
+      }
+      return id
+    }
+
+    function ensureHasId(iframeId) {
+      if (typeof iframeId !== 'string') {
+        throw new TypeError('Invaild id for iFrame. Expected String')
+      }
+
+      if ('' === iframeId) {
+        // eslint-disable-next-line no-multi-assign
+        iframe.id = iframeId = newId()
+        logEnabled = (options || {}).log
+        log(
+          iframeId,
+          'Added missing iframe ID: ' + iframeId + ' (' + iframe.src + ')'
+        )
+      }
+
+      return iframeId
+    }
+
+    function setScrolling() {
+      log(
+        iframeId,
+        'IFrame scrolling ' +
+          (settings[iframeId] && settings[iframeId].scrolling
+            ? 'enabled'
+            : 'disabled') +
+          ' for ' +
+          iframeId
+      )
+      iframe.style.overflow =
+        false === (settings[iframeId] && settings[iframeId].scrolling)
+          ? 'hidden'
+          : 'auto'
+      switch (settings[iframeId] && settings[iframeId].scrolling) {
+        case 'omit': {
+          break
+        }
+
+        case true: {
+          iframe.scrolling = 'yes'
+          break
+        }
+
+        case false: {
+          iframe.scrolling = 'no'
+          break
+        }
+
+        default: {
+          iframe.scrolling = settings[iframeId]
+            ? settings[iframeId].scrolling
+            : 'no'
+        }
+      }
+    }
+
+    // The V1 iFrame script expects an int, where as in V2 expects a CSS
+    // string value such as '1px 3em', so if we have an int for V2, set V1=V2
+    // and then convert V2 to a string PX value.
+    function setupBodyMarginValues() {
+      if (
+        'number' ===
+          typeof (settings[iframeId] && settings[iframeId].bodyMargin) ||
+        '0' === (settings[iframeId] && settings[iframeId].bodyMargin)
+      ) {
+        settings[iframeId].bodyMarginV1 = settings[iframeId].bodyMargin
+        settings[iframeId].bodyMargin =
+          '' + settings[iframeId].bodyMargin + 'px'
+      }
+    }
+
+    function checkReset() {
+      // Reduce scope of firstRun to function, because IE8's JS execution
+      // context stack is borked and this value gets externally
+      // changed midway through running this function!!!
+      var firstRun = settings[iframeId] && settings[iframeId].firstRun,
+        resetRequertMethod =
+          settings[iframeId] &&
+          settings[iframeId].heightCalculationMethod in resetRequiredMethods
+
+      if (!firstRun && resetRequertMethod) {
+        resetIFrame({ iframe: iframe, height: 0, width: 0, type: 'init' })
+      }
+    }
+
+    function setupIFrameObject() {
+      if (settings[iframeId]) {
+        settings[iframeId].iframe.iFrameResizer = {
+          close: closeIFrame.bind(null, settings[iframeId].iframe),
+
+          removeListeners: removeIframeListeners.bind(
+            null,
+            settings[iframeId].iframe
+          ),
+
+          resize: trigger.bind(
+            null,
+            'Window resize',
+            'resize',
+            settings[iframeId].iframe
+          ),
+
+          moveToAnchor: function (anchor) {
+            trigger(
+              'Move to anchor',
+              'moveToAnchor:' + anchor,
+              settings[iframeId].iframe,
+              iframeId
+            )
+          },
+
+          sendMessage: function (message) {
+            message = JSON.stringify(message)
+            trigger(
+              'Send Message',
+              'message:' + message,
+              settings[iframeId].iframe,
+              iframeId
+            )
+          }
+        }
+      }
+    }
+
+    // We have to call trigger twice, as we can not be sure if all
+    // iframes have completed loading when this code runs. The
+    // event listener also catches the page changing in the iFrame.
+    function init(msg) {
+      function iFrameLoaded() {
+        trigger('iFrame.onload', msg, iframe, undefined, true)
+        checkReset()
+      }
+
+      function createDestroyObserver(MutationObserver) {
+        if (!iframe.parentNode) {
+          return
+        }
+
+        var destroyObserver = new MutationObserver(function (mutations) {
+          mutations.forEach(function (mutation) {
+            var removedNodes = Array.prototype.slice.call(mutation.removedNodes) // Transform NodeList into an Array
+            removedNodes.forEach(function (removedNode) {
+              if (removedNode === iframe) {
+                closeIFrame(iframe)
+              }
+            })
+          })
+        })
+        destroyObserver.observe(iframe.parentNode, {
+          childList: true
+        })
+      }
+
+      var MutationObserver = getMutationObserver()
+      if (MutationObserver) {
+        createDestroyObserver(MutationObserver)
+      }
+
+      addEventListener(iframe, 'load', iFrameLoaded)
+      trigger('init', msg, iframe, undefined, true)
+    }
+
+    function checkOptions(options) {
+      if ('object' !== typeof options) {
+        throw new TypeError('Options is not an object')
+      }
+    }
+
+    function copyOptions(options) {
+      // eslint-disable-next-line no-restricted-syntax
+      for (var option in defaults) {
+        if (Object.prototype.hasOwnProperty.call(defaults, option)) {
+          settings[iframeId][option] = Object.prototype.hasOwnProperty.call(
+            options,
+            option
+          )
+            ? options[option]
+            : defaults[option]
+        }
+      }
+    }
+
+    function getTargetOrigin(remoteHost) {
+      return '' === remoteHost ||
+        null !== remoteHost.match(/^(about:blank|javascript:|file:\/\/)/)
+        ? '*'
+        : remoteHost
+    }
+
+    function depricate(key) {
+      var splitName = key.split('Callback')
+
+      if (splitName.length === 2) {
+        var name =
+          'on' + splitName[0].charAt(0).toUpperCase() + splitName[0].slice(1)
+        this[name] = this[key]
+        delete this[key]
+        warn(
+          iframeId,
+          "Deprecated: '" +
+            key +
+            "' has been renamed '" +
+            name +
+            "'. The old method will be removed in the next major version."
+        )
+      }
+    }
+
+    function processOptions(options) {
+      options = options || {}
+
+      settings[iframeId] = Object.create(null) // Protect against prototype attacks
+      settings[iframeId].iframe = iframe
+      settings[iframeId].firstRun = true
+      settings[iframeId].remoteHost =
+        iframe.src && iframe.src.split('/').slice(0, 3).join('/')
+
+      checkOptions(options)
+      Object.keys(options).forEach(depricate, options)
+      copyOptions(options)
+
+      if (settings[iframeId]) {
+        settings[iframeId].targetOrigin =
+          true === settings[iframeId].checkOrigin
+            ? getTargetOrigin(settings[iframeId].remoteHost)
+            : '*'
+      }
+    }
+
+    function beenHere() {
+      return iframeId in settings && 'iFrameResizer' in iframe
+    }
+
+    var iframeId = ensureHasId(iframe.id)
+
+    if (beenHere()) {
+      warn(iframeId, 'Ignored iFrame, already setup.')
+    } else {
+      processOptions(options)
+      setScrolling()
+      setLimits()
+      setupBodyMarginValues()
+      init(createOutgoingMsg(iframeId))
+      setupIFrameObject()
+    }
+  }
+
+  function debouce(fn, time) {
+    if (null === timer) {
+      timer = setTimeout(function () {
+        timer = null
+        fn()
+      }, time)
+    }
+  }
+
+  var frameTimer = {}
+  function debounceFrameEvents(fn, time, frameId) {
+    if (!frameTimer[frameId]) {
+      frameTimer[frameId] = setTimeout(function () {
+        frameTimer[frameId] = null
+        fn()
+      }, time)
+    }
+  }
+
+  // Not testable in PhantomJS
+  /* istanbul ignore next */
+
+  function fixHiddenIFrames() {
+    function checkIFrames() {
+      function checkIFrame(settingId) {
+        function chkDimension(dimension) {
+          return (
+            '0px' ===
+            (settings[settingId] && settings[settingId].iframe.style[dimension])
+          )
+        }
+
+        function isVisible(el) {
+          return null !== el.offsetParent
+        }
+
+        if (
+          settings[settingId] &&
+          isVisible(settings[settingId].iframe) &&
+          (chkDimension('height') || chkDimension('width'))
+        ) {
+          trigger(
+            'Visibility change',
+            'resize',
+            settings[settingId].iframe,
+            settingId
+          )
+        }
+      }
+
+      Object.keys(settings).forEach(function (key) {
+        checkIFrame(key)
+      })
+    }
+
+    function mutationObserved(mutations) {
+      log(
+        'window',
+        'Mutation observed: ' + mutations[0].target + ' ' + mutations[0].type
+      )
+      debouce(checkIFrames, 16)
+    }
+
+    function createMutationObserver() {
+      var target = document.querySelector('body'),
+        config = {
+          attributes: true,
+          attributeOldValue: false,
+          characterData: true,
+          characterDataOldValue: false,
+          childList: true,
+          subtree: true
+        },
+        observer = new MutationObserver(mutationObserved)
+
+      observer.observe(target, config)
+    }
+
+    var MutationObserver = getMutationObserver()
+    if (MutationObserver) {
+      createMutationObserver()
+    }
+  }
+
+  function resizeIFrames(event) {
+    function resize() {
+      sendTriggerMsg('Window ' + event, 'resize')
+    }
+
+    log('window', 'Trigger event: ' + event)
+    debouce(resize, 16)
+  }
+
+  // Not testable in PhantomJS
+  /* istanbul ignore next */
+  function tabVisible() {
+    function resize() {
+      sendTriggerMsg('Tab Visible', 'resize')
+    }
+
+    if ('hidden' !== document.visibilityState) {
+      log('document', 'Trigger event: Visibility change')
+      debouce(resize, 16)
+    }
+  }
+
+  function sendTriggerMsg(eventName, event) {
+    function isIFrameResizeEnabled(iframeId) {
+      return (
+        settings[iframeId] &&
+        'parent' === settings[iframeId].resizeFrom &&
+        settings[iframeId].autoResize &&
+        !settings[iframeId].firstRun
+      )
+    }
+
+    Object.keys(settings).forEach(function (iframeId) {
+      if (isIFrameResizeEnabled(iframeId)) {
+        trigger(eventName, event, settings[iframeId].iframe, iframeId)
+      }
+    })
+  }
+
+  function setupEventListeners() {
+    addEventListener(window, 'message', iFrameListener)
+
+    addEventListener(window, 'resize', function () {
+      resizeIFrames('resize')
+    })
+
+    addEventListener(document, 'visibilitychange', tabVisible)
+
+    addEventListener(document, '-webkit-visibilitychange', tabVisible)
+  }
+
+  function factory() {
+    function init(options, element) {
+      function chkType() {
+        if (!element.tagName) {
+          throw new TypeError('Object is not a valid DOM element')
+        } else if ('IFRAME' !== element.tagName.toUpperCase()) {
+          throw new TypeError(
+            'Expected <IFRAME> tag, found <' + element.tagName + '>'
+          )
+        }
+      }
+
+      if (element) {
+        chkType()
+        setupIFrame(element, options)
+        iFrames.push(element)
+      }
+    }
+
+    function warnDeprecatedOptions(options) {
+      if (options && options.enablePublicMethods) {
+        warn(
+          'enablePublicMethods option has been removed, public methods are now always available in the iFrame'
+        )
+      }
+    }
+
+    var iFrames
+
+    setupRequestAnimationFrame()
+    setupEventListeners()
+
+    return function iFrameResizeF(options, target) {
+      iFrames = [] // Only return iFrames past in on this call
+
+      warnDeprecatedOptions(options)
+
+      switch (typeof target) {
+        case 'undefined':
+        case 'string': {
+          Array.prototype.forEach.call(
+            document.querySelectorAll(target || 'iframe'),
+            init.bind(undefined, options)
+          )
+          break
+        }
+
+        case 'object': {
+          init(options, target)
+          break
+        }
+
+        default: {
+          throw new TypeError('Unexpected data type (' + typeof target + ')')
+        }
+      }
+
+      return iFrames
+    }
+  }
+
+  function createJQueryPublicMethod($) {
+    if (!$.fn) {
+      info('', 'Unable to bind to jQuery, it is not fully loaded.')
+    } else if (!$.fn.iFrameResize) {
+      $.fn.iFrameResize = function $iFrameResizeF(options) {
+        function init(index, element) {
+          setupIFrame(element, options)
+        }
+
+        return this.filter('iframe').each(init).end()
+      }
+    }
+  }
+
+  if (window.jQuery !== undefined) {
+    createJQueryPublicMethod(window.jQuery)
+  }
+
+  if (typeof define === 'function' && define.amd) {
+    define([], factory)
+  } else if (typeof module === 'object' && typeof module.exports === 'object') {
+    // Node for browserfy
+    module.exports = factory()
+  }
+  window.iFrameResize = window.iFrameResize || factory()
+})()
diff --git a/apps/routerconsole/jsp/js/iframed.js b/apps/routerconsole/jsp/js/iframed.js
index 58fe8bbdcbe51f15aa0bf73ee9c7d90f481fdd6b..f4df680b6f1046ae0cf9061341b09e3a9e86a3f9 100644
--- a/apps/routerconsole/jsp/js/iframed.js
+++ b/apps/routerconsole/jsp/js/iframed.js
@@ -38,6 +38,9 @@ function resizeFrame(f) {
 
         // Delete the div
         document.body.removeChild(scrollDiv);
+
+        // a little extra just in case there's some margin in the iframe
+        totalHeight += 20;
     }
 
     f.style.height = totalHeight + "px";
diff --git a/apps/routerconsole/jsp/torrents.jsp b/apps/routerconsole/jsp/torrents.jsp
index 1d9f46f5fbee16ad94b5a32ee090c08b948afbb9..dce6c7022b720faea9ea860e4c594dd51515bb05 100644
--- a/apps/routerconsole/jsp/torrents.jsp
+++ b/apps/routerconsole/jsp/torrents.jsp
@@ -26,6 +26,7 @@
 <%@include file="css.jsi" %>
 <%=intl.title("torrents")%>
 <script src="/js/iframed.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
+<script src="/js/iframeResizer.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <%@include file="summaryajax.jsi" %>
 <script nonce="<%=cspNonce%>" type="text/javascript">
 /* @license http://creativecommons.org/publicdomain/zero/1.0/legalcode CC0-1.0 */
@@ -35,6 +36,7 @@
       f.addEventListener("load", function() {
           injectClass(f);
           resizeFrame(f);
+          iFrameResize({ log: false }, '#i2psnarkframe')
       }, true);
   }
 
diff --git a/apps/routerconsole/jsp/webmail.jsp b/apps/routerconsole/jsp/webmail.jsp
index 889b9e152f5090c804e4c539651bddd559839c6f..598b02852b22c1d523e59801651c8f7b0d8c580f 100644
--- a/apps/routerconsole/jsp/webmail.jsp
+++ b/apps/routerconsole/jsp/webmail.jsp
@@ -26,6 +26,7 @@
 <%@include file="css.jsi" %>
 <%=intl.title("webmail")%>
 <script src="/js/iframed.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
+<script src="/js/iframeResizer.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <%@include file="summaryajax.jsi" %>
 <script nonce="<%=cspNonce%>" type="text/javascript">
 /* @license http://creativecommons.org/publicdomain/zero/1.0/legalcode CC0-1.0 */
@@ -35,6 +36,7 @@
       f.addEventListener("load", function() {
           injectClass(f);
           resizeFrame(f);
+          iFrameResize({ log: false }, '#susimailframe')
       }, true);
   }
 
diff --git a/apps/susidns/src/jsp/addressbook.jsp b/apps/susidns/src/jsp/addressbook.jsp
index eec385a3ea5eb7f9e9eea1363c09398dfd27d51e..cb54d92e24bb8c27733c068fe4a1956610738f3b 100644
--- a/apps/susidns/src/jsp/addressbook.jsp
+++ b/apps/susidns/src/jsp/addressbook.jsp
@@ -58,6 +58,7 @@
 <title>${book.book} <%=intl._t("address book")%> - susidns</title>
 <link rel="stylesheet" type="text/css" href="<%=book.getTheme()%>susidns.css?<%=net.i2p.CoreVersion.VERSION%>">
 <script src="/js/resetScroll.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
+<script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <script src="js/messages.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 </head>
 <body>
diff --git a/apps/susidns/src/jsp/config.jsp b/apps/susidns/src/jsp/config.jsp
index 8f788feaaaa429f23adc60e74dad0c4eb570ba23..ee722338ce49d25d20b2b9622f4edb52dcfe87e5 100644
--- a/apps/susidns/src/jsp/config.jsp
+++ b/apps/susidns/src/jsp/config.jsp
@@ -37,6 +37,7 @@
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title><%=intl._t("configuration")%> - susidns</title>
 <link rel="stylesheet" type="text/css" href="<%=base.getTheme()%>susidns.css?<%=net.i2p.CoreVersion.VERSION%>">
+<script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <script src="js/messages.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 </head>
 <body>
diff --git a/apps/susidns/src/jsp/details.jsp b/apps/susidns/src/jsp/details.jsp
index ceb6f30a7cbbc1ee35827d891e42e4c17efb631e..7e057e387d37b04b530e16ea3fdd9f196e6b74bb 100644
--- a/apps/susidns/src/jsp/details.jsp
+++ b/apps/susidns/src/jsp/details.jsp
@@ -34,6 +34,7 @@
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title>${book.book} <%=intl._t("address book")%> - susidns</title>
 <link rel="stylesheet" type="text/css" href="<%=book.getTheme()%>susidns.css?<%=net.i2p.CoreVersion.VERSION%>">
+<script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <script src="js/messages.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 </head>
 <body>
diff --git a/apps/susidns/src/jsp/index.jsp b/apps/susidns/src/jsp/index.jsp
index f0d4eae45fc7019f7eaba0e78f16c5d51025cf4f..c698518d4cdd2b38ee6e3dc3e63b5bf416467e4f 100644
--- a/apps/susidns/src/jsp/index.jsp
+++ b/apps/susidns/src/jsp/index.jsp
@@ -46,6 +46,7 @@
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title><%=intl._t("Introduction")%> - SusiDNS</title>
 <link rel="stylesheet" type="text/css" href="<%=base.getTheme()%>susidns.css?<%=net.i2p.CoreVersion.VERSION%>">
+<script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <script src="js/messages.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 </head>
 <body>
diff --git a/apps/susidns/src/jsp/subscriptions.jsp b/apps/susidns/src/jsp/subscriptions.jsp
index 75e3d2ffb729442bfb7e91c82302480fdb8b463c..de8057bfb384fa17cde001c506e916a0d1e6dfe3 100644
--- a/apps/susidns/src/jsp/subscriptions.jsp
+++ b/apps/susidns/src/jsp/subscriptions.jsp
@@ -36,6 +36,7 @@
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <title><%=intl._t("subscriptions")%> - susidns</title>
 <link rel="stylesheet" type="text/css" href="<%=subs.getTheme()%>susidns.css?<%=net.i2p.CoreVersion.VERSION%>">
+<script src="/js/iframeResizer.contentWindow.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 <script src="js/messages.js?<%=net.i2p.CoreVersion.VERSION%>" type="text/javascript"></script>
 </head>
 <body>
diff --git a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
index b346059ea3934d071d2515ca2a557f67611122ab..ce230391c2e97ab2fff544fa024a17057f3a8331 100644
--- a/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
+++ b/apps/susimail/src/src/i2p/susi/webmail/WebMail.java
@@ -2401,6 +2401,7 @@ public class WebMail extends HttpServlet
 					out.println( "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=2.0, user-scalable=yes\" />\n" +
 						"<link rel=\"stylesheet\" type=\"text/css\" href=\"" + sessionObject.themePath + "mobile.css?" + CoreVersion.VERSION + "\" />" );
 				}
+				out.println("<script src=\"/js/iframeResizer.contentWindow.js?" + CoreVersion.VERSION + "\" type=\"text/javascript\"></script>");
 				if(state != State.AUTH)
 					out.println("<link rel=\"stylesheet\" href=\"themes/print.css?" + CoreVersion.VERSION + "\" type=\"text/css\" media=\"print\" />");
 				if (state == State.NEW) {
diff --git a/licenses/LICENSE-Iframe-resizer.txt b/licenses/LICENSE-Iframe-resizer.txt
new file mode 100644
index 0000000000000000000000000000000000000000..06fd9040cfcda7d5b52c3e200416b78f7c6316dc
--- /dev/null
+++ b/licenses/LICENSE-Iframe-resizer.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-2023 David J. Bradshaw
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.