diff --git a/site/assets/navigation.js b/site/assets/navigation.js new file mode 100644 index 0000000..5027bdd --- /dev/null +++ b/site/assets/navigation.js @@ -0,0 +1,38 @@ +/* Build local navigation from the page itself, so headings and links cannot drift apart. */ +(() => { + const main = document.querySelector("main"); + if (!main) return; + + const headings = [...main.querySelectorAll(":scope > h2, :scope > section > h2")] + .filter((heading) => !heading.closest("details")); + if (headings.length < 3) return; + + const usedIds = new Set([...document.querySelectorAll("[id]")].map((node) => node.id)); + for (const heading of headings) { + if (!heading.id) { + const base = heading.textContent.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "section"; + let candidate = base; + let suffix = 2; + while (usedIds.has(candidate)) candidate = `${base}-${suffix++}`; + heading.id = candidate; + usedIds.add(candidate); + } + } + + const nav = document.createElement("nav"); + nav.className = "section-nav"; + nav.setAttribute("aria-label", "On this page"); + const label = document.createElement("span"); + label.className = "section-nav-label"; + label.textContent = "On this page"; + nav.append(label); + for (const heading of headings) { + const link = document.createElement("a"); + link.href = `#${heading.id}`; + link.textContent = heading.dataset.navLabel || heading.textContent; + nav.append(link); + } + + const lede = main.querySelector(":scope > .lede"); + (lede || main.querySelector(":scope > h1")).after(nav); +})(); diff --git a/site/assets/status.js b/site/assets/status.js index fe07565..d282056 100644 --- a/site/assets/status.js +++ b/site/assets/status.js @@ -32,6 +32,14 @@ const pill = (status) => const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`; +function suiteStatus(suite) { + if (!suite) return "unknown"; + if (suite.failed) return suite.reporting && suite.severity !== "critical" ? "finding" : "failed"; + if (suite.expected) return "expected"; + if (suite.passed) return "passed"; + return "skipped"; +} + /* * Where a suite's source lives, from the workflow that ran it and its fully qualified name. * @@ -93,8 +101,76 @@ function renderSummary(snapshot) { document.getElementById("run-summary").replaceChildren(pill(snapshot.status), ...runs); } +function renderStatusStory(snapshot) { + const counts = { passed: 0, failed: 0, finding: 0, expected: 0, skipped: 0 }; + for (const version of snapshot.versions) { + for (const suite of version.suites) { + for (const testCase of suite.cases) { + const kind = testCase.status === "failed" + ? suite.reporting && suite.severity !== "critical" ? "finding" : "failed" + : testCase.status; + counts[kind] = (counts[kind] || 0) + 1; + } + } + } + + const headline = counts.failed + ? `${plural(counts.failed, "critical failure")} ${counts.failed === 1 ? "needs" : "need"} attention.` + : counts.finding + ? `No critical failures. ${plural(counts.finding, "watch finding")} ${counts.finding === 1 ? "is" : "are"} worth a look.` + : "Everything expected to run passed."; + const explanation = counts.expected + ? `${plural(counts.expected, "known limitation")} behaved as predicted; these stay separate from regressions.` + : "No predicted failures were recorded in this run."; + document.getElementById("status-headline").textContent = headline; + document.getElementById("status-explanation").textContent = explanation; + + const total = Object.values(counts).reduce((sum, value) => sum + value, 0); + const ran = total - counts.skipped; + const endpoints = snapshot.endpoints || []; + const down = endpoints.filter((endpoint) => endpoint.state !== "up"); + const released = snapshot.versions.find((version) => !version.okhttpVersion.includes("SNAPSHOT")); + const snapshotVersion = snapshot.versions.find((version) => version.okhttpVersion.includes("SNAPSHOT")); + + let comparisonValue = "One version"; + let comparisonDetail = "No release-to-snapshot comparison is available."; + if (released && snapshotVersion) { + const releaseSuites = new Map(released.suites.map((suite) => [suite.name, suiteStatus(suite)])); + const shared = snapshotVersion.suites.filter((suite) => releaseSuites.has(suite.name)); + const changed = shared.filter((suite) => releaseSuites.get(suite.name) !== suiteStatus(suite)); + comparisonValue = changed.length ? plural(changed.length, "difference") : "Aligned"; + comparisonDetail = changed.length + ? `Across ${plural(shared.length, "shared suite")}; open the matrix to see them.` + : `${plural(shared.length, "shared suite")} have the same outcome in release and snapshot.`; + } + + const signal = (href, label, value, detail, tone = "") => + el("a", { className: `signal-card ${tone}`, href }, [ + el("span", { className: "signal-label", textContent: label }), + el("strong", { textContent: value }), + el("span", { className: "signal-detail", textContent: detail }), + ]); + + document.getElementById("signal-grid").replaceChildren( + signal("#suites", "Coverage", `${ran}/${total}`, `${plural(counts.skipped, "check")} not run`), + signal("#suites", "Release ↔ snapshot", comparisonValue, comparisonDetail), + signal( + "#endpoints", + "Public endpoints", + endpoints.length ? `${endpoints.length - down.length}/${endpoints.length}` : "No probes", + down.length ? `${down.map((endpoint) => endpoint.server).join(", ")} unreachable` : "Every probed endpoint is reachable", + down.length ? "signal-warning" : "", + ), + ); +} + function renderVersionCards(snapshot) { const cards = snapshot.versions.map((version) => { + const barSegment = (className, value) => { + const segment = el("span", { className }); + segment.style.flexGrow = value; + return segment; + }; const counts = el("div", { className: "counts" }, [ el("div", {}, [ el("div", { className: "n-passed", textContent: version.passed }), @@ -126,6 +202,12 @@ function renderVersionCards(snapshot) { ` · ${plural(version.suites.length, "suite")} in ${version.timeSeconds}s`, }), counts, + el("div", { className: "result-bar", title: `${version.passed} passed, ${version.failed} unexpected, ${version.expected ?? 0} expected, ${version.skipped} skipped` }, [ + barSegment("bar-passed", version.passed), + barSegment("bar-failed", version.failed), + barSegment("bar-expected", version.expected ?? 0), + barSegment("bar-skipped", version.skipped), + ]), ]); }); @@ -148,7 +230,8 @@ function renderSuiteTable(snapshot) { const rows = suiteNames.map((name) => { const any = versions.flatMap((v) => v.suites).find((s) => s.name === name); - return el("tr", {}, [ + const statuses = versions.map((version) => suiteStatus(version.suites.find((suite) => suite.name === name))); + const row = el("tr", {}, [ el("td", { className: "suite" }, any ? suiteLink(any) : name), el("td", { className: "mono", textContent: any ? any.workflow : "" }), el("td", { className: "mono", textContent: any ? any.task : "" }), @@ -160,10 +243,7 @@ function renderSuiteTable(snapshot) { const suite = version.suites.find((s) => s.name === name); if (!suite) return el("td", {}, el("span", { className: "pill unknown", textContent: "—" })); const expected = suite.expected ?? 0; - const status = suite.failed - ? suite.reporting && suite.severity !== "critical" ? "finding" : "failed" - : expected ? "expected" - : suite.passed ? "passed" : "skipped"; + const status = suiteStatus(suite); return el("td", {}, [ pill(status), el("span", { @@ -176,12 +256,61 @@ function renderSuiteTable(snapshot) { ]); }), ]); + row.dataset.name = name.toLowerCase(); + row.dataset.attention = statuses.some((status) => !["passed", "unknown"].includes(status)) ? "true" : "false"; + row.dataset.changed = new Set(statuses.filter((status) => status !== "unknown")).size > 1 ? "true" : "false"; + return row; }); document.getElementById("suite-table").replaceChildren( el("thead", {}, head), el("tbody", {}, rows), ); + + renderSuiteControls(rows); +} + +function renderSuiteControls(rows) { + const target = document.getElementById("suite-controls"); + if (!target) return; + let filter = rows.some((row) => row.dataset.attention === "true") ? "attention" : "all"; + + const count = el("span", { className: "filter-count", ariaLive: "polite" }); + const search = el("input", { + className: "suite-search", + type: "search", + placeholder: "Find a suite…", + ariaLabel: "Find a suite", + }); + const buttons = [ + ["attention", "Needs attention"], + ["changed", "Different outcomes"], + ["all", "All suites"], + ].map(([value, label]) => { + const button = el("button", { type: "button", textContent: label }); + button.dataset.filter = value; + return button; + }); + + const update = () => { + const query = search.value.trim().toLowerCase(); + let visible = 0; + for (const row of rows) { + const matchesFilter = filter === "all" || row.dataset[filter] === "true"; + const show = matchesFilter && (!query || row.dataset.name.includes(query)); + row.hidden = !show; + if (show) visible++; + } + for (const button of buttons) button.setAttribute("aria-pressed", button.dataset.filter === filter ? "true" : "false"); + count.textContent = `Showing ${visible} of ${rows.length}`; + }; + for (const button of buttons) button.addEventListener("click", () => { + filter = button.dataset.filter; + update(); + }); + search.addEventListener("input", update); + target.replaceChildren(el("div", { className: "suite-filters" }, buttons), search, count); + update(); } function renderFailures(snapshot) { @@ -297,7 +426,7 @@ function renderFailures(snapshot) { const body = document.getElementById("failure-list"); if (!items.length) { body.replaceChildren( - el("p", { textContent: "Everything ran, and everything passed." }), + el("p", { textContent: snapshot.versions.length ? "Everything ran, and everything passed." : "No results to inspect." }), ); } else { body.replaceChildren(...rendered); @@ -618,7 +747,7 @@ function renderHistory(history) { `${version.passed} passed, ${version.failed} failed, ${version.skipped ?? 0} skipped` : `${when} ${okhttpVersion}: not tested`; const source = (entry.collectedFrom || [])[0]; - return el("a", { className: `${status}`, href: source ? source.runUrl : "#", title }); + return el("a", { className: `${status}`, href: source ? source.runUrl : "#", title, ariaLabel: title }); }); return el("div", { className: "history-row" }, [ @@ -631,7 +760,21 @@ function renderHistory(history) { if (!rows.length) { target.replaceChildren(el("p", { textContent: "No history recorded yet." })); } else { - target.replaceChildren(...rows); + const recent = entries.slice(-30); + const critical = recent.filter((entry) => entry.status === "failed").length; + const firstDate = (entries[0]?.finishedAt || "").slice(0, 10); + const lastDate = (entries.at(-1)?.finishedAt || "").slice(0, 10); + target.replaceChildren( + el("div", { className: "history-summary" }, [ + el("div", {}, [el("strong", { textContent: entries.length }), el("span", { textContent: "published updates" })]), + el("div", {}, [el("strong", { textContent: critical }), el("span", { textContent: `critical ${critical === 1 ? "run" : "runs"} in the latest ${recent.length}` })]), + el("div", {}, [el("strong", { textContent: `${firstDate} → ${lastDate}` }), el("span", { textContent: "history retained" })]), + ]), + el("div", { className: "history-legend card-label" }, [ + pill("passed"), pill("finding"), pill("failed"), el("span", { textContent: "Oldest → newest; focus or hover for counts." }), + ]), + ...rows, + ); } } @@ -722,6 +865,7 @@ async function loadJson(path) { try { const snapshot = await loadJson("data/latest.json"); renderSummary(snapshot); + renderStatusStory(snapshot); renderVersionCards(snapshot); renderSuiteTable(snapshot); renderFailures(snapshot); @@ -741,6 +885,9 @@ async function loadJson(path) { ` runs. (${e.message})`, ]), ); + document.getElementById("status-headline").textContent = "No published results yet."; + document.getElementById("status-explanation").textContent = "The verdict and comparison will appear when result data is available."; + document.getElementById("signal-grid").replaceChildren(); // The sections are in the page unconditionally now, so they have to say something when // there is nothing to say. An empty table under a heading reads as a broken page. renderFailures({ versions: [] }); diff --git a/site/assets/style.css b/site/assets/style.css index 6755659..d5e601a 100644 --- a/site/assets/style.css +++ b/site/assets/style.css @@ -4,14 +4,14 @@ :root { color-scheme: light dark; - --bg: #fbfbfa; + --bg: #f7f7f4; --bg-raised: #ffffff; - --bg-sunken: #f2f2ef; - --border: #e2e2dd; - --border-strong: #cfcfc7; - --text: #1d1d1b; - --text-muted: #6a6a63; - --link: #1a5fb4; + --bg-sunken: #efefe9; + --border: #dfdfd7; + --border-strong: #c7c7bc; + --text: #20201d; + --text-muted: #686860; + --link: #175ea8; --pass: #2c7a4b; --pass-bg: #e6f4ea; @@ -24,7 +24,7 @@ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; --radius: 10px; - --measure: 68rem; + --measure: 76rem; } @media (prefers-color-scheme: dark) { @@ -55,7 +55,7 @@ body { margin: 0; background: var(--bg); color: var(--text); - font: 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + font: 16px/1.55 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-text-size-adjust: 100%; } @@ -75,6 +75,9 @@ code, .mono { font-family: var(--mono); font-size: 0.9em; } .site-header { border-bottom: 1px solid var(--border); background: var(--bg-raised); + position: sticky; + top: 0; + z-index: 10; } .site-header .wrap { @@ -111,17 +114,18 @@ code, .mono { font-family: var(--mono); font-size: 0.9em; } /* ---- page structure ---- */ -main { padding: 2.5rem 0 4rem; } +main { padding: 3.25rem 0 4rem; } h1 { - font-size: 1.9rem; - line-height: 1.2; - letter-spacing: -0.02em; - margin: 0 0 0.5rem; + font-size: clamp(2rem, 4vw, 3.35rem); + line-height: 1.05; + letter-spacing: -0.045em; + margin: 0 0 0.85rem; + max-width: 50rem; } h2 { - font-size: 1.25rem; + font-size: 1.35rem; letter-spacing: -0.01em; margin: 2.75rem 0 0.75rem; padding-bottom: 0.4rem; @@ -130,6 +134,19 @@ h2 { h3 { font-size: 1rem; margin: 1.75rem 0 0.5rem; } +h2[id], h3[id] { scroll-margin-top: 5rem; } + +.eyebrow, +.story-kicker { + display: block; + margin: 0 0 0.45rem; + color: var(--text-muted); + font-size: 0.74rem; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; +} + .lede { font-size: 1.05rem; color: var(--text-muted); @@ -137,6 +154,37 @@ h3 { font-size: 1rem; margin: 1.75rem 0 0.5rem; } margin: 0 0 2rem; } +.status-lede { font-size: 1.12rem; margin-bottom: 1.5rem; } + +.section-intro { margin: -0.25rem 0 1rem; color: var(--text-muted); font-size: 0.92rem; } + +/* Generated from the h2s, which keeps long reference pages navigable without a second TOC. */ +.section-nav { + display: flex; + align-items: center; + gap: 0; + overflow-x: auto; + margin: 0.25rem 0 1.75rem; + padding: 0.35rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-raised); + scrollbar-width: thin; +} + +.section-nav a, +.section-nav-label { + flex: 0 0 auto; + padding: 0.38rem 0.65rem; + color: var(--text-muted); + font-size: 0.8rem; + text-decoration: none; + white-space: nowrap; +} + +.section-nav-label { color: var(--text); font-weight: 700; } +.section-nav a:hover { color: var(--text); background: var(--bg-sunken); border-radius: 6px; } + p, ul, ol { max-width: 46rem; } /* ---- status pills ---- */ @@ -177,7 +225,7 @@ p, ul, ol { max-width: 46rem; } align-items: center; gap: 0.5rem 1.25rem; padding: 0.85rem 1.1rem; - margin-bottom: 2rem; + margin-bottom: 1rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-raised); @@ -185,6 +233,52 @@ p, ul, ol { max-width: 46rem; } color: var(--text-muted); } +/* ---- status story ---- */ + +.status-story { + display: grid; + grid-template-columns: minmax(18rem, 0.9fr) minmax(28rem, 1.5fr); + gap: 1rem; + margin: 1rem 0; +} + +.story-lead { + padding: 1.35rem 1.45rem; + border-radius: var(--radius); + color: #f8f8f4; + background: #242723; +} + +.story-lead .story-kicker { color: #b8c0b6; } +.story-lead h2 { margin: 0; padding: 0; border: 0; font-size: 1.55rem; line-height: 1.2; } +.story-lead p { margin: 0.75rem 0 0; color: #cbd0c8; font-size: 0.9rem; } + +.signal-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1px; + overflow: hidden; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--border); +} + +.signal-card { + display: flex; + flex-direction: column; + min-width: 0; + padding: 1.1rem; + color: inherit; + background: var(--bg-raised); + text-decoration: none; +} + +.signal-card:hover { background: var(--bg-sunken); } +.signal-card strong { margin: 0.15rem 0 0.35rem; font-size: 1.4rem; line-height: 1.15; letter-spacing: -0.025em; } +.signal-label { color: var(--text-muted); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; } +.signal-detail { color: var(--text-muted); font-size: 0.78rem; line-height: 1.4; overflow-wrap: anywhere; } +.signal-warning strong { color: var(--finding); } + .run-summary strong { color: var(--text); font-weight: 600; } /* ---- version cards ---- */ @@ -202,6 +296,22 @@ p, ul, ol { max-width: 46rem; } padding: 1.1rem 1.25rem 1.25rem; } +.result-bar { + display: flex; + width: 100%; + height: 5px; + margin-top: 1rem; + overflow: hidden; + border-radius: 999px; + background: var(--skip-bg); +} + +.result-bar span { min-width: 2px; } +.bar-passed { background: var(--pass); } +.bar-failed { background: var(--fail); } +.bar-expected { background: var(--finding); } +.bar-skipped { background: var(--border-strong); } + .card-head { display: flex; align-items: center; @@ -237,7 +347,12 @@ p, ul, ol { max-width: 46rem; } /* ---- tables ---- */ -.table-scroll { overflow-x: auto; } +.table-scroll { + overflow-x: auto; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--bg-raised); +} table { width: 100%; @@ -259,10 +374,58 @@ th { color: var(--text-muted); font-weight: 600; white-space: nowrap; + background: var(--bg-raised); } +tbody tr:last-child td { border-bottom: 0; } tbody tr:hover { background: var(--bg-sunken); } +#suite-controls { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.65rem 1rem; + margin-bottom: 0.65rem; +} + +.suite-filters { + display: inline-flex; + padding: 3px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-sunken); +} + +.suite-filters button { + border: 0; + border-radius: 5px; + padding: 0.38rem 0.7rem; + color: var(--text-muted); + background: transparent; + font: inherit; + font-size: 0.8rem; + cursor: pointer; +} + +.suite-filters button[aria-pressed="true"] { + color: var(--text); + background: var(--bg-raised); + box-shadow: 0 1px 2px rgb(0 0 0 / 0.08); +} + +.suite-search { + min-width: 13rem; + padding: 0.48rem 0.7rem; + border: 1px solid var(--border); + border-radius: 8px; + color: var(--text); + background: var(--bg-raised); + font: inherit; + font-size: 0.82rem; +} + +.filter-count { margin-left: auto; color: var(--text-muted); font-size: 0.8rem; } + td.suite { font-family: var(--mono); white-space: nowrap; } details.ech-record { @@ -406,6 +569,23 @@ details.about .pill { vertical-align: baseline; } /* ---- history strip ---- */ +.history-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + overflow: hidden; + margin-bottom: 0.75rem; + border: 1px solid var(--border); + border-radius: var(--radius); + background: var(--border); + gap: 1px; +} + +.history-summary div { display: flex; flex-direction: column; padding: 0.85rem 1rem; background: var(--bg-raised); } +.history-summary strong { font-size: 1rem; } +.history-summary span { color: var(--text-muted); font-size: 0.75rem; } +.history-legend { display: flex; align-items: center; flex-wrap: wrap; gap: 0.35rem 0.65rem; margin-bottom: 0.5rem; } +.history-legend .pill { transform: scale(0.9); transform-origin: left center; } + .history-row { display: flex; align-items: center; @@ -421,19 +601,23 @@ details.about .pill { vertical-align: baseline; } color: var(--text-muted); } -.history-strip { display: flex; gap: 2px; flex-wrap: wrap; } +.history-strip { display: flex; gap: 2px; flex-wrap: nowrap; overflow-x: auto; padding: 3px; } .history-strip a { - width: 12px; + width: 9px; + flex: 0 0 9px; height: 26px; border-radius: 2px; background: var(--skip-bg); display: block; } +.history-strip a:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; } + .history-strip a.passed { background: var(--pass); } .history-strip a.failed { background: var(--fail); } .history-strip a.finding { background: var(--finding); } +.history-strip a:last-child { box-shadow: 0 0 0 2px var(--bg-raised), 0 0 0 3px var(--border-strong); } /* ---- topic pages ---- */ @@ -520,3 +704,21 @@ details.about .pill { vertical-align: baseline; } margin: 0 0 .5rem; color: var(--text-muted); } + +@media (max-width: 760px) { + .site-header { position: static; } + .site-header .wrap { display: block; padding-left: 0; padding-right: 0; } + .site-title { display: block; padding: 0 1rem 0.65rem; } + .site-nav { flex-wrap: nowrap; overflow-x: auto; gap: 1rem; padding: 0 1rem 0.15rem; scrollbar-width: thin; } + main { padding-top: 2rem; } + .status-story { grid-template-columns: 1fr; } + .signal-grid { grid-template-columns: 1fr; } + .history-summary { grid-template-columns: 1fr; } + .cards { grid-template-columns: 1fr; } + .counts { justify-content: space-between; gap: 0.5rem; } + .filter-count { width: 100%; margin-left: 0; } +} + +@media (prefers-reduced-motion: no-preference) { + html { scroll-behavior: smooth; } +} diff --git a/site/index.html b/site/index.html index f875b9f..6bb740d 100644 --- a/site/index.html +++ b/site/index.html @@ -28,18 +28,34 @@
-

Testbed status

+

Live compatibility signal

+

OkHttp, tested against the real network.

+

+ Published releases and the next snapshot, exercised across real JDKs, Android, containers, + public resolvers and public servers. The latest evidence decides what appears here. +

Loading the most recent run…
+
+
+ Latest verdict +

Reading the latest results…

+

+
+
+
+
-
-

Failures and findings

+
+

Needs attention

Suites

+

Compare the same suite across published and snapshot versions. Start with the rows that need attention, or show the complete matrix.

+
@@ -49,27 +65,30 @@

Endpoints

-

Handshake offered

+

Protocol observations

+

Measurements are shown without turning platform or server policy into a pass/fail judgement.

+ +

Handshake offered

-

Resolvers, side by side

+

Resolvers, side by side

-

HTTP/3 offered, and taken

+

HTTP/3 offered, and taken

-

Revocation, pinning and CT

+

Revocation, pinning and CT

-

History

+

History

Open work

@@ -187,6 +206,7 @@

About these results

+ diff --git a/site/topics/dns.html b/site/topics/dns.html index 43a96f8..2d70218 100644 --- a/site/topics/dns.html +++ b/site/topics/dns.html @@ -174,6 +174,7 @@

Reading a result

+ diff --git a/site/topics/ech.html b/site/topics/ech.html index 88f8d40..044f590 100644 --- a/site/topics/ech.html +++ b/site/topics/ech.html @@ -338,6 +338,7 @@

Reading a result

+ + diff --git a/site/topics/proxies.html b/site/topics/proxies.html index 52b8f1b..34c236f 100644 --- a/site/topics/proxies.html +++ b/site/topics/proxies.html @@ -134,6 +134,7 @@

Reading a result

+ diff --git a/site/topics/test-servers.html b/site/topics/test-servers.html index 470b06d..877e611 100644 --- a/site/topics/test-servers.html +++ b/site/topics/test-servers.html @@ -465,6 +465,7 @@

Being a good citizen

+ diff --git a/site/topics/tls.html b/site/topics/tls.html index 8b1e870..8e653eb 100644 --- a/site/topics/tls.html +++ b/site/topics/tls.html @@ -163,6 +163,7 @@

Reading a result

+