diff --git a/brian_polling_manager/error_report/cli.py b/brian_polling_manager/error_report/cli.py
index d21de54007022c314ec1d1fcbb907fe30f87502b..2e59e4197324f0399b15912fb9c1d7a6d8938449 100644
--- a/brian_polling_manager/error_report/cli.py
+++ b/brian_polling_manager/error_report/cli.py
@@ -32,21 +32,18 @@ sent to the OC.
       --config PATH     Path to a config file for this tool. The schema this config
                         file must adhere to can be found in
                         ``brian_polling_manager.error_report.config.ERROR_REPORT_CONFIG_SCHEMA``
+      --email/--no-email    Either send an email using the email config, or print to
+                            stdout. Default (--email)
 
-[2024-04-09] This tool is the successor of a bash-script that was used before. That
-script has some peculiarities in it's output and as of this new version mimics the
-output of the earlier tool as much as possible.
-
-*) There are some rules which routers/interfaces to include and exclude. See the
+*) There are some rules about which routers/interfaces to include and exclude. See the
 `get_relevant_interfaces`_ function for more details.
 """
 
 from datetime import datetime
-import json
 import logging
-import os
 import pathlib
-from typing import Sequence
+import sys
+from typing import Sequence, Tuple
 from brian_polling_manager.influx import influx_client
 from brian_polling_manager.inventory import load_interfaces
 import click
@@ -143,43 +140,12 @@ PROCESSED_ERROR_COUNTERS_SCHEMA = {
 }
 
 
-LOGGING_DEFAULT_CONFIG = {
-    "version": 1,
-    "disable_existing_loggers": False,
-    "formatters": {"simple": {"format": "%(asctime)s - %(levelname)s - %(message)s"}},
-    "handlers": {
-        "console": {
-            "class": "logging.StreamHandler",
-            "level": "INFO",
-            "formatter": "simple",
-            "stream": "ext://sys.stdout",
-        },
-    },
-    "loggers": {
-        "brian_polling_manager": {
-            "level": "INFO",
-            "handlers": ["console"],
-            "propagate": False,
-        }
-    },
-    "root": {"level": "INFO", "handlers": ["console"]},
-}
-
-
 def setup_logging():
-    """
-    set up logging using the configured filename
-
-    if LOGGING_CONFIG is defined in the environment, use this for
-    the filename, otherwise use LOGGING_DEFAULT_CONFIG
-    """
-    logging_config = LOGGING_DEFAULT_CONFIG
-    if "LOGGING_CONFIG" in os.environ:
-        filename = os.environ["LOGGING_CONFIG"]
-        with open(filename) as f:
-            logging_config = json.loads(f.read())
-
-    logging.config.dictConfig(logging_config)
+    logging.basicConfig(
+        stream=sys.stderr,
+        level="INFO",
+        format="%(asctime)s - %(levelname)s - %(message)s",
+    )
 
 
 def get_error_points(client: InfluxDBClient, time_window: str):
@@ -251,6 +217,8 @@ def interface_errors(
     }
 
     result = {"interfaces": [], "excluded_interfaces": []}
+    new_errors = {}
+
     for (router, ifc), info in interface_info.items():
         try:
             today = todays_data[(router, ifc)]
@@ -273,46 +241,60 @@ def interface_errors(
             "description": info["description"],
         }
 
-        if not is_excluded_interface(info["description"], exclusions):
-            nonzero_errors = {err: val for err, val in today.items() if val > 0}
-            counters["error_counters"] = nonzero_errors
-
-            if any(yesterday.values()):
-                # we have existing errors
-
-                # This is strictly not the most correct way to determine differences.
-                # during the day the error count may have reset and diffs may actually
-                # be negative, but we ignore those because that is (mostly) how it was
-                # done in the orginal bash script
-                diff = {
-                    err: (val - yesterday[err])
-                    for err, val in nonzero_errors.items()
-                    if (val - yesterday[err]) > 0
-                }
-                if not diff:
-                    # Skip interface if it does not have any increased error counters
-                    continue
-
-                counters["diff"] = diff
-
-            result["interfaces"].append(counters)
-        else:
+        if is_excluded_interface(info, exclusions):
             logger.info(f"Found excluded interface {router} - {ifc}")
             result["excluded_interfaces"].append(counters)
+            continue
 
+        nonzero_errors = {err: val for err, val in today.items() if val > 0}
+        counters["error_counters"] = nonzero_errors
+        new_errors[(router, ifc)] = sum(nonzero_errors.values())
+
+        if any(yesterday.values()):
+            # we have existing errors
+
+            # This is strictly not the most correct way to determine differences.
+            # during the day the error count may have reset and diffs may actually
+            # be negative, but we ignore those because that is (mostly) how it was
+            # done in the orginal bash script
+            diff = {
+                err: (val - yesterday[err])
+                for err, val in nonzero_errors.items()
+                if (val - yesterday[err]) > 0
+            }
+            if not diff:
+                # Skip interface if it does not have any increased error counters
+                continue
+
+            counters["diff"] = diff
+            new_errors[(router, ifc)] = sum(diff.values())
+
+        result["interfaces"].append(counters)
+
+    result["interfaces"].sort(
+        key=lambda i: (
+            -new_errors[(i["router"], i["interface"])],
+            i["router"],
+            i["interface"],
+        ),
+    )
     return result
 
 
-def is_excluded_interface(description: str, exclusions: Sequence[str]):
+def is_excluded_interface(ifc, exclusions: Sequence[Tuple[str, str]]):
     """Some interfaces generate a lot of noise and should be excluded"""
-    # We may want to put this logic inside inventory provider
-    return any(excl.lower() in description.lower() for excl in exclusions)
+    router, interface = ifc["router"], ifc["name"]
+
+    return any(
+        router.lower() == excl[0].lower() and interface.lower() == excl[1].lower()
+        for excl in exclusions
+    )
 
 
 def get_relevant_interfaces(hosts):
     """Get interface info from inventory provider. Some interfaces are considered
-    irrelevant based on their description"""
-
+    irrelevant based on their description
+    """
     return _filter_and_sort_interfaces(load_interfaces(hosts))
 
 
@@ -338,7 +320,7 @@ def _filter_and_sort_interfaces(interfaces):
     )
 
 
-def main(config: dict):
+def main(config: dict, send_mail: bool):
     """Main function for the error reporting script
 
     :param config: An instance of `ERROR_REPORT_CONFIG_SCHEMA`
@@ -364,6 +346,10 @@ def main(config: dict):
         all_error_counters,
         date=datetime.utcnow().strftime("%a %d %b %H:%M:%S UTC %Y"),
     )
+    if not send_mail:
+        click.echo(body)
+        return
+
     email = render_email(config["email"], html=body)
     logger.info("Sending email...")
     send_email(email, config=config["email"])
@@ -383,10 +369,17 @@ def main(config: dict):
     ),
     help="path to a config file",
 )
-def cli(config):
+@click.option(
+    "--email/--no-email",
+    "send_mail",
+    default=True,
+    help="toggle sending out an email using the email configuration (--email), "
+    "or printing the report to stdout (--no-email). Default: --email",
+)
+def cli(config, send_mail):
     setup_logging()
     config = load(config_file=config)
-    main(config)
+    main(config, send_mail=send_mail)
 
 
 if __name__ == "__main__":
diff --git a/brian_polling_manager/error_report/config-example.json b/brian_polling_manager/error_report/config-example.json
index 190d08e6615fb0f2cc6f06884381605654a6d731..b9cc65d8bc8908cf43a714d1b0116359490630b4 100644
--- a/brian_polling_manager/error_report/config-example.json
+++ b/brian_polling_manager/error_report/config-example.json
@@ -18,6 +18,6 @@
     "password": "user-password"
   },
   "exclude-interfaces": [
-    "SOME DESCRIPTION PART"
+    ["some.router", "some/ifc"]
   ]
 }
diff --git a/brian_polling_manager/error_report/config.py b/brian_polling_manager/error_report/config.py
index 2614a4e191af3191867ad90466b1fb3717de69b6..f0bcc5b5d961db4a24c2a82e20335f16ca8dec52 100644
--- a/brian_polling_manager/error_report/config.py
+++ b/brian_polling_manager/error_report/config.py
@@ -17,6 +17,7 @@ ERROR_REPORT_CONFIG_SCHEMA = {
                 "to": {"$ref": "#/definitions/string-or-array"},
                 "cc": {"$ref": "#/definitions/string-or-array"},
                 "hostname": {"type": "string"},
+                "port": {"type": "integer"},
                 "username": {"type": "string"},
                 "password": {"type": "string"},
                 "starttls": {"type": "boolean"},
@@ -34,7 +35,7 @@ ERROR_REPORT_CONFIG_SCHEMA = {
                 {"type": "array", "items": {"type": "string"}, "minItems": 1},
             ]
         },
-        "influx-db-measurement": {
+        "influx-db-params": {
             "type": "object",
             "properties": {
                 "ssl": {"type": "boolean"},
@@ -64,10 +65,16 @@ ERROR_REPORT_CONFIG_SCHEMA = {
             "items": {"type": "string", "format": "uri"},
             "minItems": 1,
         },
-        "influx": {"$ref": "#/definitions/influx-db-measurement"},
+        "influx": {"$ref": "#/definitions/influx-db-params"},
         "exclude-interfaces": {
             "type": "array",
-            "items": {"type": "string"},
+            "items": {
+                "type": "array",
+                "prefixItems": [
+                    {"type": "string"},
+                    {"type": "string"},
+                ],
+            },
         },
     },
     "required": ["email", "influx"],
diff --git a/brian_polling_manager/error_report/email.jinja2 b/brian_polling_manager/error_report/email.jinja2
deleted file mode 100644
index 90a06bfe30b8005a905b3a9b795cab7d2b436c94..0000000000000000000000000000000000000000
--- a/brian_polling_manager/error_report/email.jinja2
+++ /dev/null
@@ -1,40 +0,0 @@
-{#-
-We have a lot of hardcoded stuff in here. Ideally we don't want to use jinja here
-at all but use pythons EmailMessage or MIMEBase classes to construct the email.
-However, there are a bunch of peculiarities in the email message that make it hard to
-do this, unless we can simplify the email. But for now we stick to the original email
-message format
--#}
-From: {{ config.from }}
-Sender: {{ config.from }}
-{%- if config.reply_to is defined %}
-Reply-To: {{ config.reply_to }}
-{%- endif %}
-To: {{ config.to | join('; ') }}
-{%- if config.cc is defined %}
-Cc: {{ config.cc | join('; ') }}
-{%- endif %}
-Mime-Version: 1.0
-Subject: {{ subject }}
-X-Mailer: /home/neteam/code/juniper_errors_report/email.sh
-Content-Type: multipart/mixed; boundary="-"
-
----
-Content-Type: text/plain; format=flowed; charset=ISO-8859-1
-Content-Disposition: inline
-Content-Transfer-Encoding: 8bit
-
-Hi,
-
-The latest errors report is attached.
-
-Regards,
-neteam@neteam-server01.geant.org:/home/neteam/code/juniper_errors_report
-
----
-Content-Type: text/plain; name="errors_report.html"
-Content-Transfer-Encoding: base64
-Content-Disposition: inline; filename="errors_report.html"
-Content-MD5: {{ md5_hash }}
-
-{{ b64_report | wordwrap(76) }}
\ No newline at end of file
diff --git a/brian_polling_manager/error_report/error_report.html.jinja2 b/brian_polling_manager/error_report/error_report.html.jinja2
index cb7371acbb375a023dc6fdc4262c9e3b6ff6a158..8314caecf84aa17562164ca25cbffa47061220d0 100644
--- a/brian_polling_manager/error_report/error_report.html.jinja2
+++ b/brian_polling_manager/error_report/error_report.html.jinja2
@@ -1,36 +1,91 @@
-<html>
-<body>
-<pre>
-{#- We mix tabs with spaces and have otherwise inconsistent whitespace to keep the
-    output as close as possible to the output of the original script
-#}
-{%- for ifc in interfaces  %}
-=================================
-{{ ifc.router }}
-=================================
-	{{ ifc.interface }}	         {{ ifc.description }}
-        {%- if ifc.diff %}
-        {%- for err, diff in ifc.diff.items() %}
-		{{ err }}{{ "	" if err == "framing-errors" else "" }}	{{ ifc.error_counters[err] }}		Diff:	{{ diff }}
-        {%- endfor %}
-        {%- else %}
-        {%- for err, val in ifc.error_counters.items() %}
-		{{ err }}{{ "	" if err == "framing-errors" else "" }}	{{ val }}
-        {%- endfor %}
-        {%- endif %}
-{{ '' }}
-{%- endfor %}
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>GÉANT Interface error report</title>
+    <style>
+        html {
+            font-family: monospace;
+            font-size: 12pt;
+        }
+
+        section, footer {
+            padding: 20pt 0 0 20pt;
+        }
+
+        .interface-block {
+            padding-bottom: 20pt;
+        }
 
-{%- if excluded_interfaces %}
-ROUTER,INTERFACE,FRAMING ERRORS,BIT ERROR SECONDS,ERRORED BLOCKS SECONDS,CRC ERRORS,TOTAL ERRORS,INPUT DISCARDS,INPUT DROPS,OUTPUT DROPS
-{%- for ifc in excluded_interfaces %}
-{{ifc.router}},{{ifc.interface}},{{ ifc.error_counters.values() | join(',') }},{{ifc.description}}
-{%- endfor %}
-{%- endif %}
+        .interface-errors {
+            padding-left: 20pt;
+        }
 
+        ul {
+            list-style-type: none;
+        }
 
+        h3, p, ul {
+            margin: 0pt;
+        }
 
-Generated {{ date }}
-</pre>
-</body>
+        .error-table {
+            padding-left: 20pt;
+        }
+
+        td {
+            padding-right: 10pt;
+        }
+
+        td.diff {
+            padding-left: 20pt;
+        }
+
+    </style>
+  </head>
+  <body>
+    <section>
+      <div class="interface-report">
+        {%- for ifc in interfaces %}
+        <div class="interface-block">
+          <h3>{{ ifc.router }}</h3>
+          <div class="interface-errors">
+            <p><strong>{{ ifc.interface }}</strong> {{ ifc.description }}</p>
+            <table class="error-table">
+              {%- if ifc.diff %}
+                {%- for err, diff in ifc.diff.items() %}
+                  <tr>
+                    <td>{{ err }}</td>
+                    <td>{{ ifc.error_counters[err] }}</td>
+                    <td class="diff">Diff:</td>
+                    <td>{{ diff }}</td>
+                  </tr>
+                {%- endfor %}
+              {%- else %}
+                {%- for err, val in ifc.error_counters.items() %}
+                  <tr>
+                    <td>{{ err }}</td>
+                    <td>{{ val }}</td>
+                  </tr>
+                {%- endfor %}
+              {%- endif %}
+            </table>
+          </div>
+        </div>
+        {%- endfor %}
+      </div>
+    </section>
+    {%- if excluded_interfaces %}
+      <section class="excluded">
+        <p>EXCLUDED INTERFACES</p>
+        <ul class="interface-list">
+          {%- for ifc in excluded_interfaces %}
+            <li>{{ ifc.router }} - {{ ifc.interface }} - {{ ifc.description }}</li>
+          {%- endfor %}
+        </ul>
+      </section>
+    {%- endif %}
+    <footer>Generated {{ date }}</footer>
+  </body>
 </html>
diff --git a/brian_polling_manager/error_report/report.py b/brian_polling_manager/error_report/report.py
index 3c1764a64498f0912950c7c079be04f241b84e39..946d4594491216b47efe499ce6ff6968b4051cff 100644
--- a/brian_polling_manager/error_report/report.py
+++ b/brian_polling_manager/error_report/report.py
@@ -1,9 +1,11 @@
+from email import encoders
+from email.mime.base import MIMEBase
+from email.mime.multipart import MIMEMultipart
+from email.mime.text import MIMEText
 import logging
 import pathlib
 import smtplib
 import jinja2
-import hashlib
-import base64
 
 THIS_DIR = pathlib.Path(__file__).parent
 logger = logging.getLogger(__name__)
@@ -18,32 +20,43 @@ def render_html(errors, date):
     :param errors: an instance of `PROCESSED_ERROR_COUNTERS_SCHEMA`
     :param template_file: a jinja2 template to render
     """
-    env = jinja2.Environment(
-        # use CRLF since that was (explicitly) used by the original bash script, perhaps
-        # it's unnecessary
-        loader=jinja2.FileSystemLoader(THIS_DIR),
-        newline_sequence="\r\n",
-    )
+    env = jinja2.Environment(loader=jinja2.FileSystemLoader(THIS_DIR))
     template = env.get_template("error_report.html.jinja2")
     return template.render(**errors, date=date)
 
 
 def render_email(
-    email_config: dict, html: str, subject="GEANT Juniper Interface Errors Report"
+    email_config: dict, html: str, subject="GEANT Interface Errors Report"
 ):
-    env = jinja2.Environment(loader=jinja2.FileSystemLoader(THIS_DIR))
-    template = env.get_template("email.jinja2")
-    html_as_bytes = html.encode()
-    md5_hash = hashlib.md5(html_as_bytes).hexdigest()
-    b64_report = base64.b64encode(html_as_bytes).decode()
-    return template.render(
-        config=email_config, subject=subject, md5_hash=md5_hash, b64_report=b64_report
-    )
+
+    body = "The latest errors report is attached."
+
+    message = MIMEMultipart()
+    message["From"] = email_config["from"]
+    message["To"] = "; ".join(email_config["to"])
+    if email_config.get("cc"):
+        message["Cc"] = "; ".join(email_config["cc"])
+
+    message["Subject"] = subject
+
+    message.attach(MIMEText(body, "plain"))
+
+    part = MIMEBase("application", "octet-stream")
+    part.set_payload(html.encode())
+
+    encoders.encode_base64(part)
+
+    part.add_header("Content-Disposition", "attachment; filename= error_report.html")
+
+    message.attach(part)
+    return message.as_string()
 
 
 def send_email(payload: str, config: dict):
     with smtplib.SMTP(
-        host=config["hostname"], port=25, timeout=SMTP_TIMEOUT_SECONDS
+        host=config["hostname"],
+        port=config.get("port", 25),
+        timeout=SMTP_TIMEOUT_SECONDS,
     ) as server:
         if config.get("starttls"):
             server.starttls()
diff --git a/test/error_report/data/test_render_html-expected.html b/test/error_report/data/test_render_html-expected.html
index 3c6cdeda8b64e04fe798180d90c6b2047cf9ff3c..3a0a88538edef7819b70efcdac582dfd1458fabd 100644
--- a/test/error_report/data/test_render_html-expected.html
+++ b/test/error_report/data/test_render_html-expected.html
@@ -1,23 +1,86 @@
-<html>
-<body>
-<pre>
-=================================
-mx1.ams.nl.geant.net
-=================================
-	ae1	         PHY blah blah
-		framing-errors		4		Diff:	2
-		input-drops	2		Diff:	1
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>GÉANT Interface error report</title>
+    <style>
+        html {
+            font-family: monospace;
+            font-size: 12pt;
+        }
 
-=================================
-mx1.fra.de.geant.net
-=================================
-	ae10	         PHY blah blah foo
-		input-drops	3
+        section, footer {
+            padding: 20pt 0 0 20pt;
+        }
 
+        .interface-block {
+            padding-bottom: 20pt;
+        }
 
+        .interface-errors {
+            padding-left: 20pt;
+        }
 
+        ul {
+            list-style-type: none;
+        }
 
-Generated <some date>
-</pre>
-</body>
+        h3, p, ul {
+            margin: 0pt;
+        }
+
+        .error-table {
+            padding-left: 20pt;
+        }
+
+        td {
+            padding-right: 10pt;
+        }
+
+        td.diff {
+            padding-left: 20pt;
+        }
+
+    </style>
+  </head>
+  <body>
+    <section>
+      <div class="interface-report">
+        <div class="interface-block">
+          <h3>mx1.ams.nl.geant.net</h3>
+          <div class="interface-errors">
+            <p><strong>ae1</strong> PHY blah blah</p>
+            <table class="error-table">
+                  <tr>
+                    <td>framing-errors</td>
+                    <td>4</td>
+                    <td class="diff">Diff:</td>
+                    <td>2</td>
+                  </tr>
+                  <tr>
+                    <td>input-drops</td>
+                    <td>2</td>
+                    <td class="diff">Diff:</td>
+                    <td>1</td>
+                  </tr>
+            </table>
+          </div>
+        </div>
+        <div class="interface-block">
+          <h3>mx1.fra.de.geant.net</h3>
+          <div class="interface-errors">
+            <p><strong>ae10</strong> PHY blah blah foo</p>
+            <table class="error-table">
+                  <tr>
+                    <td>input-drops</td>
+                    <td>3</td>
+                  </tr>
+            </table>
+          </div>
+        </div>
+      </div>
+    </section>
+    <footer>Generated SOME_DATE</footer>
+  </body>
 </html>
\ No newline at end of file
diff --git a/test/error_report/data/test_render_html_with_exclusions-expected.html b/test/error_report/data/test_render_html_with_exclusions-expected.html
index bcd4e358a74e3f8cdc4c5a38862be7731845e9b7..2369a6fc18b5575f8cf77c68631943946a5c2d72 100644
--- a/test/error_report/data/test_render_html_with_exclusions-expected.html
+++ b/test/error_report/data/test_render_html_with_exclusions-expected.html
@@ -1,18 +1,72 @@
-<html>
-<body>
-<pre>
-=================================
-mx1.ams.nl.geant.net
-=================================
-	ae1	         PHY blah blah
-		input-drops	2
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <title>GÉANT Interface error report</title>
+    <style>
+        html {
+            font-family: monospace;
+            font-size: 12pt;
+        }
 
-ROUTER,INTERFACE,FRAMING ERRORS,BIT ERROR SECONDS,ERRORED BLOCKS SECONDS,CRC ERRORS,TOTAL ERRORS,INPUT DISCARDS,INPUT DROPS,OUTPUT DROPS
-mx1.fra.de.geant.net,ae10,1,2,3,4,5,6,7,8,PHY blah blah foo
+        section, footer {
+            padding: 20pt 0 0 20pt;
+        }
 
+        .interface-block {
+            padding-bottom: 20pt;
+        }
 
+        .interface-errors {
+            padding-left: 20pt;
+        }
 
-Generated <some date>
-</pre>
-</body>
+        ul {
+            list-style-type: none;
+        }
+
+        h3, p, ul {
+            margin: 0pt;
+        }
+
+        .error-table {
+            padding-left: 20pt;
+        }
+
+        td {
+            padding-right: 10pt;
+        }
+
+        td.diff {
+            padding-left: 20pt;
+        }
+
+    </style>
+  </head>
+  <body>
+    <section>
+      <div class="interface-report">
+        <div class="interface-block">
+          <h3>mx1.ams.nl.geant.net</h3>
+          <div class="interface-errors">
+            <p><strong>ae1</strong> PHY blah blah</p>
+            <table class="error-table">
+                  <tr>
+                    <td>input-drops</td>
+                    <td>2</td>
+                  </tr>
+            </table>
+          </div>
+        </div>
+      </div>
+    </section>
+      <section class="excluded">
+        <p>EXCLUDED INTERFACES</p>
+        <ul class="interface-list">
+            <li>mx1.fra.de.geant.net - ae10 - PHY blah blah foo</li>
+        </ul>
+      </section>
+    <footer>Generated SOME_DATE</footer>
+  </body>
 </html>
\ No newline at end of file
diff --git a/test/error_report/test_error_report.py b/test/error_report/test_error_report.py
index 08e1dee23792b3b16c7d94cfef7e55dedea172da..4865948f1bfb47730207e3c9356e998a5d683573 100644
--- a/test/error_report/test_error_report.py
+++ b/test/error_report/test_error_report.py
@@ -1,5 +1,4 @@
 import base64
-import hashlib
 import json
 
 import pathlib
@@ -161,7 +160,7 @@ def test_validate_config(tmp_path):
             "username": "some-username",
             "password": "user-password",
         },
-        "exclude-interfaces": ["SOME DESCRIPTION PART"],
+        "exclude-interfaces": [["some.router", "some/ifc"]],
     }
     config_file.write_text(json.dumps(content))
     result = config.load(config_file)
@@ -184,7 +183,7 @@ def test_validate_config(tmp_path):
             "username": "some-username",
             "password": "user-password",
         },
-        "exclude-interfaces": ["SOME DESCRIPTION PART"],
+        "exclude-interfaces": [["some.router", "some/ifc"]],
     }
 
 
@@ -206,18 +205,19 @@ def test_get_relevant_interfaces(full_inventory):
 
 
 @pytest.mark.parametrize(
-    "description, exclusions, is_excluded",
+    "router, interface, exclusions, is_excluded",
     [
-        ("DESC", [], False),
-        ("DESC", ["DESC"], True),
-        ("DESC", ["desc"], True),
-        ("DESC", ["DESCMORE"], False),
-        ("DESC", ["MORE", "DESC"], True),
-        ("", ["DESC"], False),
+        ("rtr", "ifc", [], False),
+        ("rtr", "ifc", [("rtr", "ifc")], True),
+        ("rtr", "ifc", [("RTR", "IFC")], True),
+        ("rtr", "ifc", [("rtr", "ifc.2")], False),
+        ("rtr", "ifc", [("rtr", "ifc.2"), ("rtr", "ifc")], True),
+        ("rtr", "ifc", [("rtr2", "ifc")], False),
     ],
 )
-def test_excluded_interface(description, exclusions, is_excluded):
-    assert is_excluded_interface(description, exclusions) == is_excluded
+def test_excluded_interface(router, interface, exclusions, is_excluded):
+    interface = {"router": router, "name": interface}
+    assert is_excluded_interface(interface, exclusions) == is_excluded
 
 
 def test_get_error_points(mock_influx_client, create_error_point):
@@ -287,8 +287,8 @@ def test_interface_errors_with_new_errors(create_error_point, get_interface_erro
 def test_interface_errors_with_multiple_interfaces(
     create_error_point, get_interface_errors
 ):
-    create_error_point("mx1.ams.nl.geant.net", "ae1", "today", framing_errors=1)
-    create_error_point("mx1.fra.de.geant.net", "ae10", "today", framing_errors=2)
+    create_error_point("mx1.ams.nl.geant.net", "ae1", "today", framing_errors=2)
+    create_error_point("mx1.fra.de.geant.net", "ae10", "today", framing_errors=1)
     errors = get_interface_errors()
     assert errors["interfaces"] == [
         {
@@ -296,7 +296,7 @@ def test_interface_errors_with_multiple_interfaces(
             "interface": "ae1",
             "description": "PHY blah blah",
             "error_counters": {
-                "framing-errors": 1,
+                "framing-errors": 2,
             },
         },
         {
@@ -304,7 +304,7 @@ def test_interface_errors_with_multiple_interfaces(
             "interface": "ae10",
             "description": "PHY blah blah foo",
             "error_counters": {
-                "framing-errors": 2,
+                "framing-errors": 1,
             },
         },
     ]
@@ -400,7 +400,7 @@ def test_processes_excluded_interface(create_error_point, get_interface_errors):
         "mx1.fra.de.geant.net", "ae10", "today", input_drops=3, framing_errors=4
     )  # this interface is excluded through its description
 
-    errors = get_interface_errors(exclusions=["foo"])
+    errors = get_interface_errors(exclusions=[("mx1.fra.de.geant.net", "ae10")])
     assert errors["interfaces"] == [
         {
             "router": "mx1.ams.nl.geant.net",
@@ -428,6 +428,41 @@ def test_processes_excluded_interface(create_error_point, get_interface_errors):
     ]
 
 
+def test_orders_errors_by_highest_number(create_error_point, get_interface_errors):
+    create_error_point(
+        "mx1.ams.nl.geant.net", "ae1", "today", input_drops=10, framing_errors=2
+    )
+    create_error_point(
+        "mx1.fra.de.geant.net", "ae10", "today", input_drops=10, framing_errors=2
+    )
+    create_error_point(
+        "mx1.ams.nl.geant.net", "ae1", "yesterday", input_drops=1, framing_errors=2
+    )
+    errors = get_interface_errors()
+    interfaces = [(e["router"], e["interface"]) for e in errors["interfaces"]]
+    assert interfaces == [
+        ("mx1.fra.de.geant.net", "ae10"),
+        ("mx1.ams.nl.geant.net", "ae1"),
+    ]
+
+
+def test_some_more_order_test(create_error_point, get_interface_errors):
+    create_error_point(
+        "mx1.ams.nl.geant.net", "ae1", "yesterday", input_drops=1, framing_errors=2
+    )
+    create_error_point(
+        "mx1.ams.nl.geant.net", "ae1", "today", input_drops=2, framing_errors=4
+    )
+
+    create_error_point("mx1.fra.de.geant.net", "ae10", "today", input_drops=3)
+    errors = get_interface_errors()
+    interfaces = [(e["router"], e["interface"]) for e in errors["interfaces"]]
+    assert interfaces == [
+        ("mx1.ams.nl.geant.net", "ae1"),
+        ("mx1.fra.de.geant.net", "ae10"),
+    ]
+
+
 def test_render_html(create_error_point, get_interface_errors):
     create_error_point(
         "mx1.ams.nl.geant.net", "ae1", "yesterday", input_drops=1, framing_errors=2
@@ -438,11 +473,11 @@ def test_render_html(create_error_point, get_interface_errors):
 
     create_error_point("mx1.fra.de.geant.net", "ae10", "today", input_drops=3)
     errors = get_interface_errors()
-    result = render_html(errors=errors, date="<some date>")
+    result = render_html(errors=errors, date="SOME_DATE")
     # The expected value contains mixed tabs and spaces. We put it in a separate file
     # to comply with flake8
     expected = (DATA_DIR / "test_render_html-expected.html").read_text()
-    assert result == expected.replace("\n", "\r\n")
+    assert result == expected
 
 
 def test_render_html_with_exclusions(create_error_point, get_interface_errors):
@@ -452,35 +487,25 @@ def test_render_html_with_exclusions(create_error_point, get_interface_errors):
         "mx1.fra.de.geant.net",
         "ae10",
         "today",
-        # mess up order of kwargs to test re-ordering
-        bit_error_seconds=2,
         framing_errors=1,
-        input_crc_errors=4,
-        errored_blocks_seconds=3,
-        input_discards=6,
-        input_total_errors=5,
-        output_drops=8,
-        input_drops=7,
     )
-    errors = get_interface_errors(exclusions=["foo"])
-    result = render_html(errors=errors, date="<some date>")
+    errors = get_interface_errors(exclusions=[("mx1.fra.de.geant.net", "ae10")])
+    result = render_html(errors=errors, date="SOME_DATE")
     # The expected value contains mixed tabs and spaces. We put it in a separate file
     # to comply with flake8
     expected = (DATA_DIR / "test_render_html_with_exclusions-expected.html").read_text()
 
-    assert result == expected.replace("\n", "\r\n")
+    assert result == expected
 
 
 def test_render_email():
     body = "<SOME_BODY>"
-    md5 = hashlib.md5(body.encode()).hexdigest()
     b64 = base64.b64encode(body.encode()).decode()
     config = {"from": "someone@geant.org", "to": ["someone.else@geant.org"]}
     result = render_email(config, html=body, subject="<subject>")
 
     assert "From: someone@geant.org" in result
     assert "To: someone.else@geant.org" in result
-    assert md5 in result
     assert b64 in result
 
 
@@ -590,7 +615,7 @@ def config_file(tmp_path):
             "username": "some-username",
             "password": "user-password",
         },
-        "exclude-interfaces": ["FOO"],
+        "exclude-interfaces": [("mx1.fra.de.geant.net", "ae10")],
     }
     path = tmp_path / "config.json"
     path.write_text(json.dumps(config))
@@ -623,9 +648,9 @@ def test_e2e(
 
         assert "The latest errors report is attached." in payload
 
-        report_b64 = payload.split("\n\n")[-1]
+        report_b64 = payload.split("\n\n")[-2]
         report = base64.b64decode(report_b64).decode()
 
         assert "mx1.ams.nl.geant.net" in report
         assert "ae1" in report
-        assert "input-drops\t1" in report
+        assert "<td>input-drops</td>" in report