Further refactor
diff --git a/gr-zuul-summary-status/gr-zuul-summary-status-view-tab-header-view.js b/gr-zuul-summary-status/gr-zuul-summary-status-view-tab-header-view.js
deleted file mode 100644
index 81a680a..0000000
--- a/gr-zuul-summary-status/gr-zuul-summary-status-view-tab-header-view.js
+++ /dev/null
@@ -1,14 +0,0 @@
-class GrZuulSummaryStatusChangeViewTabHeaderView extends Polymer.Element {
-
-    static get is() {
-        return 'gr-zuul-summary-status-view-tab-header-view';
-    }
-
-   static get template() {
-     return Polymer.html`Zuul Summary`;
-   }
- }
-
-customElements.define(GrZuulSummaryStatusChangeViewTabHeaderView.is,
-                      GrZuulSummaryStatusChangeViewTabHeaderView);
-
diff --git a/gr-zuul-summary-status/gr-zuul-summary-status-view.js b/gr-zuul-summary-status/gr-zuul-summary-status-view.js
deleted file mode 100644
index 78e0495..0000000
--- a/gr-zuul-summary-status/gr-zuul-summary-status-view.js
+++ /dev/null
@@ -1,152 +0,0 @@
-class GrZuulSummaryStatusView extends Polymer.Element {
-
-    static get is() {
-        return 'gr-zuul-summary-status-view'
-    }
-
-    static get properties() {
-        return {
-            change: Object,
-            revision: Object
-        }
-    }
-
-    static get template() {
-        return Polymer.html`
-  <style>
-    table {
-      table-layout: fixed;
-      width: 100%;
-      border-collapse: collapse;
-    }
-
-    th, td {
-      text-align: left;
-      padding: 10px;
-    }
-
-    th {
-      background-color: #f7ffff;
-    }
-
-    tr:nth-child(even) {
-     background-color: #f2f2f2;
-    }
-
-    .status-SUCCESS {
-      color: green;
-    }
-
-    .status-FAILURE {
-      color: red;
-    }
-  </style>
-
-  <template is="dom-repeat" items="[[__table]]">
-   <table>
-    <tr>
-     <th>[[item.author]] in pipeline [[item.pipeline]] ([[item.rechecks]] rechecks)</th>
-     <th>[[item.date]]</th>
-    </tr>
-    <template is="dom-repeat" items="[[item.results]]" as="job">
-     <tr>
-      <td><a href="[[job.link]]">[[job.job]]</a></td>
-      <td><span class$="status-[[job.result]]">[[job.result]]</span> in [[job.time]]</td>
-     </tr>
-    </template>
-   </table>
-  </template>
-`;
-    }
-
-    ready() {
-        super.ready();
-        /*
-         * change-view-tab-content gets passed ChangeInfo object [1],
-         * registered in the property "change".  We walk the list of
-         * messages with some regexps to extract into a datastructure
-         * stored in __table
-         *
-         * __table is an [] of objects
-         *
-         *  author: "<string> CI"
-         *  date: date message posted
-         *  status: one of <succeeded|failed>
-         *  pipeline: string of reporting pipeline
-         *    (may be undefined for some CI)
-         *  results: [] of objects
-         *    job: job name
-         *    link: raw URL link to logs
-         *    result: one of <SUCCESS|FAILURE>
-         *    time: duration of run in human string (e.g. 2m 5s)
-         *
-         * This is then presented by the template
-         *
-         * [1] https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
-         */
-        this.__table = [];
-        this.change.messages.forEach((element) => {
-            if (element.author.name) {
-                /* First check it looks like Zuul, or a third-party CI
-                 * Zuul author */
-                let authorRe = /^(?<author>.* CI|Zuul)/;
-                var author = authorRe.exec(element.author.name);
-                if (author) {
-
-                    /* If we've already seen a comment by this CI,
-                     * take note of it and increase the rechecks */
-                    let existing = this.__table.findIndex((entry) => {
-                        entry.author === author });
-                    let rechecks = existing == -1 ? 0 : (this.__table[existing].rechecks + 1);
-
-                    var date = new Date(element.date);
-                    /* Look for the build status message, e.g.:
-                     *   Build succeeded (check pipeline).
-                     */
-                    let statusRe = /^Build (?<status>\w+) \((?<pipeline>[\w]+) pipeline\)\./gm
-                    let status = statusRe.exec(element.message);
-                    if (!status) {
-                        /* Match non-pipeline CI comments, e.g.:
-                         *   Build succeeded.
-                         */
-                        let statusRe = /^Build (?<status>\w+)\./gm
-                        status = statusRe.exec(element.message)
-                    }
-                    if (status) {
-                        var table = {
-                            'author': author.groups.author,
-                            'rechecks': rechecks,
-                            'date': date.toLocaleString(),
-                            'status': status.groups.status,
-                            'pipeline': status.groups.pipeline,
-                            'results': []
-                        }
-                        /* Find each result line, e.g. :
-                         *   - openstack-tox-py35 http://... : SUCCESS in 2m 45
-                         */
-                        let lines = element.message.split("\n");
-                        let resultRe = /^- (?<job>[^ ]+) (?<link>[^ ]+) : (?<result>[^ ]+) in (?<time>.*)/
-                        lines.forEach((line) => {
-                            var result = resultRe.exec(line);
-                            if (result) {
-                                table.results.push(result.groups);
-                            }
-                        });
-                        if (existing == -1) {
-                            this.__table.push(table);
-                        } else {
-                            this.__table[existing] = table;
-                        }
-                        console.debug("Zuul Summary table now:");
-                        console.debug(this.__table);
-                    }
-                }
-            }
-        });
-    }
-
-
-}
-
-customElements.define(GrZuulSummaryStatusView.is,
-                      GrZuulSummaryStatusView)
diff --git a/gr-zuul-summary-status/gr-zuul-summary-status.js b/gr-zuul-summary-status/gr-zuul-summary-status.js
index 6047674..61de196 100644
--- a/gr-zuul-summary-status/gr-zuul-summary-status.js
+++ b/gr-zuul-summary-status/gr-zuul-summary-status.js
@@ -1,20 +1,237 @@
-import './gr-zuul-summary-status-view-tab-header-view.js';
-import './gr-zuul-summary-status-view.js';
+// Copyright (c) 2020 Red Hat
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may
+// not use this file except in compliance with the License. You may obtain
+// a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations
+// under the License.
 
-function installZuulSummaryStatus(plugin) {
+/*
+ * Tab contents
+ */
+class ZuulSummaryStatusTab extends Polymer.Element {
 
+    static get properties() {
+        return {
+            change: Object,
+            revision: Object
+        }
+    }
+
+    static get template() {
+        return Polymer.html`
+  <style>
+    table {
+      table-layout: fixed;
+      width: 100%;
+      border-collapse: collapse;
+    }
+
+    th, td {
+      text-align: left;
+      padding: 10px;
+    }
+
+    th {
+      background-color: #f7ffff;
+      font-weight: normal;
+      color: rgb(33, 33, 33);
+    }
+
+    tr:nth-child(even) {
+     background-color: #f2f2f2;
+    }
+
+    .status-SUCCESS {
+      color: green;
+    }
+
+    .status-FAILURE {
+      color: red;
+    }
+  </style>
+
+  <template is="dom-repeat" items="[[__table]]">
+   <table>
+    <tr>
+     <th><b>[[item.author_name]]</b> on revision <b>[[item.revision]]</b> in pipeline <b>[[item.pipeline]]</b></th>
+     <th><template is="dom-if" if="{{item.rechecks}}">[[item.rechecks]] rechecks</template></th>
+     <th><b>[[item.date]]</b></th>
+    </tr>
+    <template is="dom-repeat" items="[[item.results]]" as="job">
+     <tr>
+      <td><a href="[[job.link]]">[[job.job]]</a></td>
+      <td><span class$="status-[[job.result]]">[[job.result]]</span></td>
+      <td>[[job.time]]</td>
+     </tr>
+    </template>
+   </table>
+  </template>`;
+    }
+
+    _match_message_via_tag(message) {
+        return (message.tag &&
+                message.tag.startsWith('autogenerated:zuul')) ? true : false;
+    }
+
+    _match_message_via_regex(message) {
+        // TODO: allow this to be passed in via config
+        let authorRe = /^(?<author>.* CI|Zuul)/;
+        let author = authorRe.exec(message.author.name);
+        return author ? true : false;
+    }
+
+    _get_status_and_pipeline(message) {
+        /* Look for the full Zuul-3ish build status message, e.g.:
+         *   Build succeeded (check pipeline).
+         */
+        let statusRe = /^Build (?<status>\w+) \((?<pipeline>[\w]+) pipeline\)\./gm
+        let statusMatch = statusRe.exec(message.message);
+        if (!statusMatch) {
+            /* Match non-pipeline CI comments, e.g.:
+             *   Build succeeded.
+             */
+            let statusRe = /^Build (?<status>\w+)\./gm
+            statusMatch = statusRe.exec(message.message)
+        }
+        if (!statusMatch) {
+            return false; // we can't parse this
+        }
+
+        let status = statusMatch.groups.status
+        let pipeline = statusMatch.groups.pipeline ?
+            statusMatch.groups.pipeline : 'unknown';
+        return [status, pipeline]
+    }
+
+    ready() {
+        super.ready();
+        /*
+         * change-view-tab-content gets passed ChangeInfo object [1],
+         * registered in the property "change".  We walk the list of
+         * messages with some regexps to extract into a datastructure
+         * stored in __table
+         *
+         * __table is an [] of objects
+         *
+         *  author: "<string> CI"
+         *  date: date message posted
+         *  status: one of <succeeded|failed>
+         *  pipeline: string of reporting pipeline
+         *    (may be undefined for some CI)
+         *  results: [] of objects
+         *    job: job name
+         *    link: raw URL link to logs
+         *    result: one of <SUCCESS|FAILURE>
+         *    time: duration of run in human string (e.g. 2m 5s)
+         *
+         * This is then presented by the template
+         *
+         * [1] https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#change-info
+         */
+        this.__table = [];
+        this.change.messages.forEach((message) => {
+
+            if  (! (this._match_message_via_tag(message) ||
+                    this._match_message_via_regex(message))) {
+                return;
+            }
+
+            let date = new Date(message.date);
+            let revision = message._revision_number;
+            let sp = this._get_status_and_pipeline(message);
+            if (!sp) {
+                // This shouldn't happen as we've validated it is a Zuul message.
+                return;
+            }
+            let status = sp[0];
+            let pipeline = sp[1];
+
+            // We only want the latest entry for each CI system in
+            // each pipeline
+            let existing = this.__table.findIndex(entry =>
+                (entry.author_id === message.author._account_id) &&
+                    (entry.pipeline === pipeline));
+
+            // If this is a comment by the same CI on the same pipeline and
+            // the same revision, it's considered a "recheck" ... i.e. likely
+            // manually triggered to run again.  Take a note of this.
+            let rechecks = 0
+            if (existing != -1) {
+                if (this.__table[existing].revision === revision) {
+                    rechecks = this.__table[existing].rechecks + 1;
+                }
+            }
+
+            /* Find each result line, e.g. :
+             *   - openstack-tox-py35 http://... : SUCCESS in 2m 45
+             */
+            let results = [];
+            let lines = message.message.split("\n");
+            let resultRe = /^- (?<job>[^ ]+) (?<link>[^ ]+) : (?<result>[^ ]+) in (?<time>.*)/
+            lines.forEach((line) => {
+                let result = resultRe.exec(line);
+                if (result) {
+                    results.push(result.groups);
+                }
+            });
+
+            let table = {
+                'author_name': message.author.name,
+                'author_id': message.author._account_id,
+                'revision': revision,
+                'rechecks': rechecks,
+                'date': date.toLocaleString(),
+                'status': status,
+                'pipeline': pipeline,
+                'results': results
+            }
+
+            if (existing == -1) {
+                this.__table.push(table);
+            } else {
+                this.__table[existing] = table;
+            }
+            console.debug("Zuul Summary table now:");
+            console.debug(this.__table);
+        });
+    }
+}
+
+customElements.define('zuul-summary-status-tab',
+                      ZuulSummaryStatusTab);
+
+/*
+ * Tab Header Element
+ */
+class ZuulSummaryStatusTabHeader extends Polymer.Element {
+   static get template() {
+     return Polymer.html`Zuul Summary`;
+   }
+ }
+
+customElements.define('zuul-summary-status-tab-header',
+                      ZuulSummaryStatusTabHeader);
+
+
+/*
+ * Install plugin
+ */
+
+Gerrit.install(plugin => {
     plugin.registerDynamicCustomComponent(
         'change-view-tab-header',
-        'gr-zuul-summary-status-view-tab-header-view'
+        'zuul-summary-status-tab-header'
     );
 
     plugin.registerDynamicCustomComponent(
         'change-view-tab-content',
-        'gr-zuul-summary-status-view'
+        'zuul-summary-status-tab'
     );
-}
-
-// Install the plugin
-Gerrit.install(plugin => {
-    installZuulSummaryStatus(plugin);
 });