diff --git a/netlink-html.pl b/netlink-html.pl
index 65b055f..ebec88c 100755
--- a/netlink-html.pl
+++ b/netlink-html.pl
@@ -333,6 +333,7 @@ sub write_html_hier_files {
html_header($html, "OpenBSD Netlink Hierarchie",
"OpenBSD netlink $short test results",
@nav);
+ print $html "\n";
my @hv = sort { $a->{key} cmp $b->{key} } @{$H{$date}};
html_hier_top($html, $date, @hv);
diff --git a/tables.js b/tables.js
new file mode 100644
index 0000000..9938517
--- /dev/null
+++ b/tables.js
@@ -0,0 +1,157 @@
+/*
+* Copyright (c) 2024 Moritz Buhl
+*
+* Permission to use, copy, modify, and distribute this software for any
+* purpose with or without fee is hereby granted, provided that the above
+* copyright notice and this permission notice appear in all copies.
+*
+* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+'use strict';
+
+function merge_rows() {
+ const utilization = document.getElementsByClassName('utilization')[0];
+ const otbody = utilization.children[1];
+ const tbody = otbody.cloneNode(1);
+
+ let otr = [];
+ for (const trc of tbody.children) {
+ const tr = trc.children;
+ const len = trc.childElementCount;
+ if (!otr.length) {
+ for (let i = 0; i < len; i++) {
+ otr[i] = tr[i];
+ }
+ continue;
+ }
+
+ if (len != otr.length) {
+ console.warn(`inhomogeneous cell count: ${len} vs ${otr.length}.`);
+ return;
+ }
+ for (let i = 0; i < len; i++) {
+ if (tr[i].innerText && otr[i].innerText == tr[i].innerText) {
+ let rowspan = Number(otr[i].getAttribute('rowspan'));
+ if (!rowspan) {
+ rowspan = 1;
+ }
+ otr[i].setAttribute('rowspan', rowspan + 1);
+ tr[i].hidden = 1;
+ } else {
+ tr[i].hidden = 0;
+ otr[i] = tr[i];
+ }
+ }
+ }
+
+ otbody.replaceWith(tbody);
+}
+
+function sort_by_col(ev) {
+ const utilization = document.getElementsByClassName('utilization')[0];
+ const otbody = utilization.children[1];
+ const tbody = otbody.cloneNode(0);
+ const thead = utilization.children[0].children[0].children;
+ let col;
+ let rows = [];
+
+ for (col = 0; col < thead.length; col++) {
+ if (thead[col] == ev.target) break;
+ }
+
+ for (const tr of otbody.children) {
+ rows.push(tr);
+ }
+
+ rows.sort((a,b) => {
+ let c = col;
+ do {
+ if (c > 5)
+ c = 1;
+ if (a.children[c].innerText == b.children[c].innerText) {
+ c++;
+ } else
+ break;
+ } while (c != col);
+
+ /* XXX: return >= depending on arrow */
+ return a.children[c].innerText < b.children[c].innerText;
+ });
+
+ for (const tr of rows) {
+ tbody.appendChild(tr);
+ }
+
+ tbody.innerHTML = tbody.innerHTML.replaceAll('hidden=""', '').replaceAll(
+ 'rowspan=', 'foo='); // XXX
+
+ otbody.replaceWith(tbody);
+ merge_rows();
+}
+
+function filter_by_target(ev) {
+ const utilization = document.getElementsByClassName('utilization')[0];
+ const const otbody = utilization.children[1];
+ const tbody = otbody.cloneNode(0);
+ const par = ev.target.parentElement.children;
+ let col;
+ let match = [], others = [];
+
+ for (col = 0; col < par.length; col++) {
+ if (par[col] == ev.target) break;
+ }
+
+ for (const tr of otbody.children) {
+ if (tr.children[col].innerText == ev.target.innerText)
+ match.push(tr);
+ else
+ others.push(tr);
+ }
+
+ for (const tr of match) {
+ tbody.appendChild(tr);
+ }
+ for (const tr of others) {
+ tbody.appendChild(tr);
+ }
+
+ tbody.innerHTML = tbody.innerHTML.replaceAll('hidden=""', '').replaceAll(
+ 'rowspan=', 'foo='); // XXX
+
+ otbody.replaceWith(tbody);
+ merge_rows();
+ const descs = utilization.children[1].getElementsByClassName('desc');
+ for (let i = 0; i < descs.length; i++) {
+ let td = descs[i];
+ td.onclick = filter_by_target;
+ }
+}
+
+window.addEventListener("load", () => {
+ merge_rows();
+ const utilization = document.getElementsByClassName('utilization')[0];
+ const thead_tr = utilization.children[0].children[0].children;
+ const descs = utilization.children[1].getElementsByClassName('desc');
+
+ thead_tr[1].innerHTML = "IP";
+ thead_tr[2].innerHTML = "Transport";
+ thead_tr[3].innerHTML = "Direction";
+ thead_tr[4].innerHTML = "Test";
+ thead_tr[5].innerHTML = "Modifier";
+ for (let i = 1; i <= 5; i++) {
+ thead_tr[i].innerHTML += " →";
+ thead_tr[i].classList.add("desc");
+ /* XXX: add on-click listeners for td.descs */
+ thead_tr[i].addEventListener("click", sort_by_col);
+ }
+ for (let i = 0; i < descs.length; i++) {
+ let td = descs[i];
+ td.onclick = filter_by_target;
+ }
+});