diff mbox series

[1/1] support/scripts/pkg-stats: migrate to CSS grid and inline javascript

Message ID 20220707233115.1377820-1-sen@phobosdpl.com
State Superseded
Headers show
Series [1/1] support/scripts/pkg-stats: migrate to CSS grid and inline javascript | expand

Commit Message

Sen Hastings July 7, 2022, 11:31 p.m. UTC
This migrates pkg-stats.html from html tables to CSS grid, allowing
the use of newer, simpler javascript that is short enough to be
inlined, instead of relying on externally hosted javascript.

Javascript sorting function was rewritten from scratch in ~60 lines,
short enough to be inlined directly in the html.

Tables were redone in CSS grid, but with care taken to mimic existing
"look and feel" of prevous implementation, albeit with slightly
better responsive behavior and default styling characteristics.

Column labels are now "sticky" and stay stuck to the top of the
viewport as you scroll down the page.

Also, css was rewritten in fewer lines and table elements were changed
to divs (for grid support).

Other small misc fixes include quoted hrefs and document language
declarations to make the w3c html validator happy.

Signed-off-by: Sen Hastings <sen@phobosdpl.com>
---
 support/scripts/pkg-stats | 409 +++++++++++++++++++++-----------------
 1 file changed, 227 insertions(+), 182 deletions(-)
diff mbox series

Patch

diff --git a/support/scripts/pkg-stats b/support/scripts/pkg-stats
index f67be0063f..1d7a146abc 100755
--- a/support/scripts/pkg-stats
+++ b/support/scripts/pkg-stats
@@ -718,89 +718,117 @@  def calculate_stats(packages):
 
 
 html_header = """
+<!DOCTYPE html>
+<html lang="en">
 <head>
-<script src=\"https://www.kryogenix.org/code/browser/sorttable/sorttable.js\"></script>
-<style type=\"text/css\">
-table {
-  width: 100%;
-}
-td {
-  border: 1px solid black;
-}
-td.centered {
-  text-align: center;
-}
-td.wrong {
-  background: #ff9a69;
-}
-td.correct {
-  background: #d2ffc4;
-}
-td.nopatches {
-  background: #d2ffc4;
-}
-td.somepatches {
-  background: #ffd870;
-}
-td.lotsofpatches {
-  background: #ff9a69;
-}
-
-td.good_url {
-  background: #d2ffc4;
-}
-td.missing_url {
-  background: #ffd870;
-}
-td.invalid_url {
-  background: #ff9a69;
-}
-
-td.version-good {
-  background: #d2ffc4;
-}
-td.version-needs-update {
-  background: #ff9a69;
-}
-td.version-unknown {
- background: #ffd870;
-}
-td.version-error {
- background: #ccc;
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<script>
+function sortGrid(sortLabel){
+	pkgSortArray = [];
+	pkgStringSortArray = [];
+	pkgNumSortArray = [];
+	let columnValues = Array.from(document.getElementsByClassName(sortLabel));
+	columnLabel = columnValues.shift();
+	let pkgNameAndVal = {pkgName: "", pkgVal: ""};
+	columnValues.forEach((listing) => {
+		let val;
+		let sortObj = Object.create(pkgNameAndVal);
+		sortObj.pkgName = listing.id.replace(sortLabel+"_", "");
+		if (!listing.innerText){
+			val = -1;
+		} else {
+			val = listing.innerText;
+		};
+		sortObj.pkgVal = val;
+		pkgSortArray.push(sortObj);
+	})
+	pkgSortArray.forEach((listing) => {
+		if ( isNaN(parseInt(listing.pkgVal, 10)) ){
+			pkgStringSortArray.push(listing);
+		} else {
+			let parsedVal = parseFloat(listing.pkgVal);
+			listing.pkgVal = parsedVal;
+			pkgNumSortArray.push(listing);
+		};
+	})
+	sortedStringPkgArray = pkgStringSortArray.sort(function(a, b) {
+		const nameA = a.pkgVal.toUpperCase(); // ignore upper and lowercase
+		const nameB = b.pkgVal.toUpperCase(); // ignore upper and lowercase
+		if (nameA < nameB) { return -1;	};
+		if (nameA > nameB) { return 1; };
+		return 0;   // names must be equal
+	});
+
+	sortedNumPkgArray = pkgNumSortArray.sort(function(a, b) {
+		return a.pkgVal - b.pkgVal;
+	});
+	let columnName = document.getElementById(sortLabel);
+	if (columnName.lastElementChild.innerText == " ▾") {
+		columnName.lastElementChild.innerText = " ▴";
+		sortedStringPkgArray.reverse();
+		sortedNumPkgArray.reverse();
+		sortedPkgArray = sortedNumPkgArray.concat(sortedStringPkgArray)
+	} else if (columnName.lastElementChild.innerText == " ▴") {
+		columnName.lastElementChild.innerText = " ▾";
+		sortedPkgArray = sortedStringPkgArray.concat(sortedNumPkgArray)
+	} else {
+		columnName.lastElementChild.innerText = " ▾";
+		sortedPkgArray = sortedStringPkgArray.concat(sortedNumPkgArray)
+	}
+	sortedPkgArray.forEach((listing) => {
+		let row = Array.from(document.getElementsByClassName(listing.pkgName));
+		let packageGrid = document.getElementById("package-grid");
+		row.forEach((element) => { packageGrid.append(element)});
+	})
 }
+</script>
 
-td.cpe-ok {
-  background: #d2ffc4;
-}
+<style>
 
-td.cpe-nok {
-  background: #ff9a69;
+.label {
+  position: sticky;
+  top: 1px;
 }
-
-td.cpe-unknown {
- background: #ffd870;
+.label{
+  background: white;
+  padding: 10px 2px 10px 2px;
 }
-
-td.cve-ok {
-  background: #d2ffc4;
+#package-grid, #results-grid {
+  display: grid;
+  grid-gap: 2px;
+  grid-template-columns: 1fr repeat(12, min-content);
 }
-
-td.cve-nok {
-  background: #ff9a69;
+#results-grid {
+  grid-template-columns: 3fr 1fr;
 }
-
-td.cve-unknown {
- background: #ffd870;
+.data {
+  border: solid 1px gray;
 }
-
-td.cve_ignored {
- background: #ccc;
+.centered {
+  text-align: center;
 }
+ .wrong, .lotsofpatches, .invalid_url, .version-needs-update, .cpe-nok, .cve-nok {
+   background: #ff9a69;
+ }
+ .correct, .nopatches, .good_url, .version-good, .cpe-ok, .cve-ok {
+   background: #d2ffc4;
+ }
+ .somepatches, .missing_url, .version-unknown, .cpe-unknown, .cve-unknown {
+   background: #ffd870;
+ }
+ .cve_ignored, .version-error {
+  background: #ccc;
+ }
 
 </style>
+
 <title>Statistics of Buildroot packages</title>
+
 </head>
 
+<body>
+
 <a href=\"#results\">Results</a><br/>
 
 <p id=\"sortable_hint\"></p>
@@ -808,13 +836,13 @@  td.cve_ignored {
 
 
 html_footer = """
-</body>
 <script>
-if (typeof sorttable === \"object\") {
-  document.getElementById(\"sortable_hint\").innerHTML =
-  \"hint: the table can be sorted by clicking the column headers\"
+if (typeof sortGrid === "function") {
+  document.getElementById("sortable_hint").innerHTML =
+  "hint: the table can be sorted by clicking the column headers"
 }
 </script>
+</body>
 </html>
 """
 
@@ -841,73 +869,87 @@  def boolean_str(b):
 
 
 def dump_html_pkg(f, pkg):
-    f.write(" <tr>\n")
-    f.write("  <td>%s</td>\n" % pkg.path)
-
+    f.write( f'<div id=\"package_{pkg.name}\" \
+	class=\"package data {pkg.name}\">{pkg.path}</div>\n')
     # Patch count
-    td_class = ["centered"]
+    data_field_id = f'patch_count_{pkg.name}'
+    div_class = ["centered patch_count data"]
+    div_class.append(pkg.name)
     if pkg.patch_count == 0:
-        td_class.append("nopatches")
+        div_class.append("nopatches")
     elif pkg.patch_count < 5:
-        td_class.append("somepatches")
+        div_class.append("somepatches")
     else:
-        td_class.append("lotsofpatches")
-    f.write("  <td class=\"%s\">%s</td>\n" %
-            (" ".join(td_class), str(pkg.patch_count)))
+        div_class.append("lotsofpatches")
+    f.write( f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
+	\">{str(pkg.patch_count)}</div>\n')
 
     # Infrastructure
+    data_field_id = f'infrastructure_{pkg.name}'
     infra = infra_str(pkg.infras)
-    td_class = ["centered"]
+    div_class = ["centered infrastructure data"]
+    div_class.append(pkg.name)
     if infra == "Unknown":
-        td_class.append("wrong")
+        div_class.append("wrong")
     else:
-        td_class.append("correct")
-    f.write("  <td class=\"%s\">%s</td>\n" %
-            (" ".join(td_class), infra_str(pkg.infras)))
+        div_class.append("correct")
+    f.write( f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
+	\">{infra_str(pkg.infras)}</div>\n')
 
     # License
-    td_class = ["centered"]
+    data_field_id = f'license_{pkg.name}'
+    div_class = ["centered license data"]
+    div_class.append(pkg.name)
     if pkg.is_status_ok('license'):
-        td_class.append("correct")
+        div_class.append("correct")
     else:
-        td_class.append("wrong")
-    f.write("  <td class=\"%s\">%s</td>\n" %
-            (" ".join(td_class), boolean_str(pkg.is_status_ok('license'))))
+        div_class.append("wrong")
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
+	\">{boolean_str(pkg.is_status_ok("license"))}</div>\n')
 
     # License files
-    td_class = ["centered"]
+    data_field_id = f'license_files_{pkg.name}'
+    div_class = ["centered license_files data"]
+    div_class.append(pkg.name)
     if pkg.is_status_ok('license-files'):
-        td_class.append("correct")
+        div_class.append("correct")
     else:
-        td_class.append("wrong")
-    f.write("  <td class=\"%s\">%s</td>\n" %
-            (" ".join(td_class), boolean_str(pkg.is_status_ok('license-files'))))
+        div_class.append("wrong")
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
+	\">{boolean_str(pkg.is_status_ok("license-files"))}</div>\n')
 
     # Hash
-    td_class = ["centered"]
+    data_field_id = f'hash_file_{pkg.name}'
+    div_class = ["centered hash_file data"]
+    div_class.append(pkg.name)
     if pkg.is_status_ok('hash'):
-        td_class.append("correct")
+        div_class.append("correct")
     else:
-        td_class.append("wrong")
-    f.write("  <td class=\"%s\">%s</td>\n" %
-            (" ".join(td_class), boolean_str(pkg.is_status_ok('hash'))))
+        div_class.append("wrong")
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)} \
+	\">{boolean_str(pkg.is_status_ok("hash"))}</div>\n')
 
     # Current version
+    data_field_id = f'current_version_{pkg.name}'
     if len(pkg.current_version) > 20:
         current_version = pkg.current_version[:20] + "..."
     else:
         current_version = pkg.current_version
-    f.write("  <td class=\"centered\">%s</td>\n" % current_version)
+    f.write(f'  <div id=\"{data_field_id}\" \
+	class=\"centered current_version data {pkg.name}\">{current_version}</div>\n')
 
     # Latest version
+    data_field_id = f'latest_version_{pkg.name}'
+    div_class.append(pkg.name)
+    div_class.append("latest_version data")
     if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
-        td_class.append("version-error")
+        div_class.append("version-error")
     if pkg.latest_version['version'] is None:
-        td_class.append("version-unknown")
+        div_class.append("version-unknown")
     elif pkg.latest_version['version'] != pkg.current_version:
-        td_class.append("version-needs-update")
+        div_class.append("version-needs-update")
     else:
-        td_class.append("version-good")
+        div_class.append("version-good")
 
     if pkg.latest_version['status'] == RM_API_STATUS_ERROR:
         latest_version_text = "<b>Error</b>"
@@ -927,74 +969,81 @@  def dump_html_pkg(f, pkg):
         else:
             latest_version_text += "found by guess"
 
-    f.write("  <td class=\"%s\">%s</td>\n" %
-            (" ".join(td_class), latest_version_text))
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">{latest_version_text}</div>\n')
 
     # Warnings
-    td_class = ["centered"]
+    data_field_id = f'warnings_{pkg.name}'
+    div_class = ["centered warnings data"]
+    div_class.append(pkg.name)
     if pkg.warnings == 0:
-        td_class.append("correct")
+        div_class.append("correct")
     else:
-        td_class.append("wrong")
-    f.write("  <td class=\"%s\">%d</td>\n" %
-            (" ".join(td_class), pkg.warnings))
+        div_class.append("wrong")
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">{pkg.warnings}</div>\n')
 
     # URL status
-    td_class = ["centered"]
+    data_field_id = f'upstream_url_{pkg.name}'
+    div_class = ["centered upstream_url data"]
+    div_class.append(pkg.name)
     url_str = pkg.status['url'][1]
     if pkg.status['url'][0] in ("error", "warning"):
-        td_class.append("missing_url")
+        div_class.append("missing_url")
     if pkg.status['url'][0] == "error":
-        td_class.append("invalid_url")
-        url_str = "<a href=%s>%s</a>" % (pkg.url, pkg.status['url'][1])
+        div_class.append("invalid_url")
+        url_str = "<a href=\"%s\">%s</a>" % (pkg.url, pkg.status['url'][1])
     else:
-        td_class.append("good_url")
-        url_str = "<a href=%s>Link</a>" % pkg.url
-    f.write("  <td class=\"%s\">%s</td>\n" %
-            (" ".join(td_class), url_str))
+        div_class.append("good_url")
+        url_str = "<a href=\"%s\">Link</a>" % pkg.url
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">{url_str}</div>\n')
 
     # CVEs
-    td_class = ["centered"]
+    data_field_id = f'cves_{pkg.name}'
+    div_class = ["centered cves data"]
+    div_class.append(pkg.name)
     if pkg.is_status_ok("cve"):
-        td_class.append("cve-ok")
+        div_class.append("cve-ok")
     elif pkg.is_status_error("cve"):
-        td_class.append("cve-nok")
+        div_class.append("cve-nok")
     elif pkg.is_status_na("cve") and not pkg.is_actual_package:
-        td_class.append("cve-ok")
+        div_class.append("cve-ok")
     else:
-        td_class.append("cve-unknown")
-    f.write("  <td class=\"%s\">\n" % " ".join(td_class))
+        div_class.append("cve-unknown")
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">\n')
     if pkg.is_status_error("cve"):
         for cve in pkg.cves:
-            f.write("   <a href=\"https://security-tracker.debian.org/tracker/%s\">%s<br/>\n" % (cve, cve))
+            f.write("   <a href=\"https://security-tracker.debian.org/tracker/%s\">%s</a><br/>\n" % (cve, cve))
         for cve in pkg.unsure_cves:
-            f.write("   <a href=\"https://security-tracker.debian.org/tracker/%s\">%s <i>(unsure)</i><br/>\n" % (cve, cve))
+            f.write("   <a href=\"https://security-tracker.debian.org/tracker/%s\">%s <i>(unsure)</i></a><br/>\n" % (cve, cve))
     elif pkg.is_status_na("cve"):
         f.write("    %s" % pkg.status['cve'][1])
     else:
         f.write("    N/A\n")
-    f.write("  </td>\n")
+    f.write("  </div>\n")
 
     # CVEs Ignored
-    td_class = ["centered"]
+    data_field_id = f'ignored_cves_{pkg.name}'
+    div_class = ["centered data ignored_cves"]
+    div_class.append(pkg.name)
     if pkg.ignored_cves:
-        td_class.append("cve_ignored")
-    f.write("  <td class=\"%s\">\n" % " ".join(td_class))
+        div_class.append("cve_ignored")
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">\n')
     for ignored_cve in pkg.ignored_cves:
-        f.write("    <a href=\"https://security-tracker.debian.org/tracker/%s\">%s<br/>\n" % (ignored_cve, ignored_cve))
-    f.write("  </td>\n")
+        f.write("    <a href=\"https://security-tracker.debian.org/tracker/%s\">%s</a><br/>\n" % (ignored_cve, ignored_cve))
+    f.write("  </div>\n")
 
     # CPE ID
-    td_class = ["left"]
+    data_field_id = f'cpe_id_{pkg.name}'
+    div_class = ["left cpe_id data"]
+    div_class.append(pkg.name)
     if pkg.is_status_ok("cpe"):
-        td_class.append("cpe-ok")
+        div_class.append("cpe-ok")
     elif pkg.is_status_error("cpe"):
-        td_class.append("cpe-nok")
+        div_class.append("cpe-nok")
     elif pkg.is_status_na("cpe") and not pkg.is_actual_package:
-        td_class.append("cpe-ok")
+        div_class.append("cpe-ok")
     else:
-        td_class.append("cpe-unknown")
-    f.write("  <td class=\"%s\">\n" % " ".join(td_class))
+        div_class.append("cpe-unknown")
+    f.write(f'  <div id=\"{data_field_id}\" class=\"{" ".join(div_class)}\">\n')
     if pkg.cpeid:
         f.write("  <code>%s</code>\n" % pkg.cpeid)
     if not pkg.is_status_ok("cpe"):
@@ -1008,79 +1057,75 @@  def dump_html_pkg(f, pkg):
         else:
             f.write("  %s\n" % pkg.status['cpe'][1])
 
-    f.write("  </td>\n")
-
-    f.write(" </tr>\n")
+    f.write("  </div>\n")
 
 
 def dump_html_all_pkgs(f, packages):
     f.write("""
-<table class=\"sortable\">
-<tr>
-<td>Package</td>
-<td class=\"centered\">Patch count</td>
-<td class=\"centered\">Infrastructure</td>
-<td class=\"centered\">License</td>
-<td class=\"centered\">License files</td>
-<td class=\"centered\">Hash file</td>
-<td class=\"centered\">Current version</td>
-<td class=\"centered\">Latest version</td>
-<td class=\"centered\">Warnings</td>
-<td class=\"centered\">Upstream URL</td>
-<td class=\"centered\">CVEs</td>
-<td class=\"centered\">CVEs Ignored</td>
-<td class=\"centered\">CPE ID</td>
-</tr>
+<div id=\"package-grid\">
+<div style="grid-column: 1;" onclick="sortGrid(this.id)" id=\"package\" class=\"package data label\"><span>Package</span><span></span></div>
+<div style="grid-column: 2;" onclick="sortGrid(this.id)" id=\"patch_count\" class=\"centered patch_count data label\"><span>Patch count</span><span></span></div>
+<div style="grid-column: 3;" onclick="sortGrid(this.id)" id=\"infrastructure\" class=\"centered infrastructure data label\">Infrastructure<span></span></div>
+<div style="grid-column: 4;" onclick="sortGrid(this.id)" id=\"license\" class=\"centered license data label\"><span>License</span><span></span></div>
+<div style="grid-column: 5;" onclick="sortGrid(this.id)" id=\"license_files\" class=\"centered license_files data label\"><span>License files</span><span></span></div>
+<div style="grid-column: 6;" onclick="sortGrid(this.id)" id=\"hash_file\" class=\"centered hash_file data label\"><span>Hash file</span><span></span></div>
+<div style="grid-column: 7;" onclick="sortGrid(this.id)" id=\"current_version\" class=\"centered current_version data label\"><span>Current version</span><span></span></div>
+<div style="grid-column: 8;" onclick="sortGrid(this.id)" id=\"latest_version\" class=\"centered latest_version data label\"><span>Latest version</span><span></span></div>
+<div style="grid-column: 9;" onclick="sortGrid(this.id)" id=\"warnings\" class=\"centered warnings data label\"><span>Warnings</span><span></span></div>
+<div style="grid-column: 10;" onclick="sortGrid(this.id)" id=\"upstream_url\" class=\"centered upstream_url data label\"><span>Upstream URL</span><span></span></div>
+<div style="grid-column: 11;" onclick="sortGrid(this.id)" id=\"cves\" class=\"centered cves data label\"><span>CVEs</span><span></span></div>
+<div style="grid-column: 12;" onclick="sortGrid(this.id)" id=\"ignored_cves\" class=\"centered ignored_cves data label\"><span>CVEs Ignored</span><span></span></div>
+<div style="grid-column: 13;" onclick="sortGrid(this.id)" id=\"cpe_id\" class=\"centered cpe_id data label\"><span>CPE ID</span><span></span></div>
 """)
     for pkg in sorted(packages):
         dump_html_pkg(f, pkg)
-    f.write("</table>")
+    f.write("</div>")
 
 
 def dump_html_stats(f, stats):
     f.write("<a id=\"results\"></a>\n")
-    f.write("<table>\n")
+    f.write("<div class=\"data\" id=\"results-grid\">\n")
     infras = [infra[6:] for infra in stats.keys() if infra.startswith("infra-")]
     for infra in infras:
-        f.write(" <tr><td>Packages using the <i>%s</i> infrastructure</td><td>%s</td></tr>\n" %
+        f.write(" <div class=\"data\">Packages using the <i>%s</i> infrastructure</div><div class=\"data\">%s</div>\n" %
                 (infra, stats["infra-%s" % infra]))
-    f.write(" <tr><td>Packages having license information</td><td>%s</td></tr>\n" %
+    f.write(" <div class=\"data\">Packages having license information</div><div class=\"data\">%s</div>\n" %
             stats["license"])
-    f.write(" <tr><td>Packages not having license information</td><td>%s</td></tr>\n" %
+    f.write(" <div class=\"data\">Packages not having license information</div><div class=\"data\">%s</div>\n" %
             stats["no-license"])
-    f.write(" <tr><td>Packages having license files information</td><td>%s</td></tr>\n" %
+    f.write(" <div class=\"data\">Packages having license files information</div><div class=\"data\">%s</div>\n" %
             stats["license-files"])
-    f.write(" <tr><td>Packages not having license files information</td><td>%s</td></tr>\n" %
+    f.write(" <div class=\"data\">Packages not having license files information</div><div class=\"data\">%s</div>\n" %
             stats["no-license-files"])
-    f.write(" <tr><td>Packages having a hash file</td><td>%s</td></tr>\n" %
+    f.write(" <div class=\"data\">Packages having a hash file</div><div class=\"data\">%s</div>\n" %
             stats["hash"])
-    f.write(" <tr><td>Packages not having a hash file</td><td>%s</td></tr>\n" %
+    f.write(" <div class=\"data\">Packages not having a hash file</div><div class=\"data\">%s</div>\n" %
             stats["no-hash"])
-    f.write(" <tr><td>Total number of patches</td><td>%s</td></tr>\n" %
+    f.write(" <div class=\"data\">Total number of patches</div><div class=\"data\">%s</div>\n" %
             stats["patches"])
-    f.write("<tr><td>Packages having a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages having a mapping on <i>release-monitoring.org</i></div><div class=\"data\">%s</div>\n" %
             stats["rmo-mapping"])
-    f.write("<tr><td>Packages lacking a mapping on <i>release-monitoring.org</i></td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages lacking a mapping on <i>release-monitoring.org</i></div><div class=\"data\">%s</div>\n" %
             stats["rmo-no-mapping"])
-    f.write("<tr><td>Packages that are up-to-date</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages that are up-to-date</div><div class=\"data\">%s</div>\n" %
             stats["version-uptodate"])
-    f.write("<tr><td>Packages that are not up-to-date</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages that are not up-to-date</div><div class=\"data\">%s</div>\n" %
             stats["version-not-uptodate"])
-    f.write("<tr><td>Packages with no known upstream version</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages with no known upstream version</div><div class=\"data\">%s</div>\n" %
             stats["version-unknown"])
-    f.write("<tr><td>Packages affected by CVEs</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages affected by CVEs</div><div class=\"data\">%s</div>\n" %
             stats["pkg-cves"])
-    f.write("<tr><td>Total number of CVEs affecting all packages</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Total number of CVEs affecting all packages</div><div class=\"data\">%s</div>\n" %
             stats["total-cves"])
-    f.write("<tr><td>Packages affected by unsure CVEs</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages affected by unsure CVEs</div><div class=\"data\">%s</div>\n" %
             stats["pkg-unsure-cves"])
-    f.write("<tr><td>Total number of unsure CVEs affecting all packages</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Total number of unsure CVEs affecting all packages</div><div class=\"data\">%s</div>\n" %
             stats["total-unsure-cves"])
-    f.write("<tr><td>Packages with CPE ID</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages with CPE ID</div><div class=\"data\">%s</div>\n" %
             stats["cpe-id"])
-    f.write("<tr><td>Packages without CPE ID</td><td>%s</td></tr>\n" %
+    f.write("<div class=\"data\">Packages without CPE ID</div><div class=\"data\">%s</div>\n" %
             stats["no-cpe-id"])
-    f.write("</table>\n")
+    f.write("</div>\n")
 
 
 def dump_html_gen_info(f, date, commit):