Add HTTP headers checker and tests

This commit is contained in:
Keith Casko
2026-07-23 05:53:14 -04:00
parent 820f271195
commit 7b77c2261a
7 changed files with 1219 additions and 172 deletions
+350 -54
View File
@@ -2,16 +2,21 @@
// Tool tab navigation
// ---------------------------------------------------------
// Find all tool buttons and tool panels.
const toolTabs = document.querySelectorAll(".tool-tab");
const toolPanels = document.querySelectorAll(".tool-panel");
// Switch tools when a tab button is clicked.
toolTabs.forEach((tab) => {
tab.addEventListener("click", () => {
const selectedToolId = tab.dataset.tool;
// Update the active tab.
// Mark only the selected tab as active.
toolTabs.forEach((currentTab) => {
currentTab.classList.toggle("active", currentTab === tab);
currentTab.classList.toggle(
"active",
currentTab === tab
);
});
// Show only the selected tool panel.
@@ -29,58 +34,82 @@ toolTabs.forEach((tab) => {
const uptimeForm = document.getElementById("uptime-form");
const urlInput = document.getElementById("url-input");
const checkButton = document.getElementById("check-button");
const statusMessage = document.getElementById("status-message");
const statusMessage = document.getElementById(
"status-message"
);
const resultPanel = document.getElementById("result-panel");
const onlineBadge = document.getElementById("online-badge");
const statusCode = document.getElementById("status-code");
const responseTime = document.getElementById("response-time");
const redirectCount = document.getElementById("redirect-count");
const responseTime = document.getElementById(
"response-time"
);
const redirectCount = document.getElementById(
"redirect-count"
);
const checkedAt = document.getElementById("checked-at");
const finalUrl = document.getElementById("final-url");
// Handle Website Status form submissions.
uptimeForm.addEventListener("submit", async (event) => {
event.preventDefault();
const url = urlInput.value.trim();
// Reset the interface before checking.
statusMessage.textContent = "Checking website...";
statusMessage.className = "status-message";
resultPanel.hidden = true;
checkButton.disabled = true;
checkButton.textContent = "Checking...";
try {
const response = await fetch("/api/uptime", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
});
const result = await response.json();
// Handle HTTP-level errors.
if (!response.ok) {
throw new Error(result.error || "The request failed.");
throw new Error(
result.error || "The request failed."
);
}
// Handle unreachable or blocked destinations.
if (!result.online) {
throw new Error(result.error || "The website could not be reached.");
throw new Error(
result.error ||
"The website could not be reached."
);
}
// Fill in the successful result.
onlineBadge.textContent = "Online";
onlineBadge.className = "status-badge online";
statusCode.textContent = result.statusCode;
responseTime.textContent = `${result.responseTimeMs} ms`;
responseTime.textContent =
`${result.responseTimeMs} ms`;
redirectCount.textContent = result.redirectCount;
checkedAt.textContent = new Date(result.checkedAt).toLocaleString();
checkedAt.textContent =
new Date(result.checkedAt).toLocaleString();
finalUrl.textContent = result.finalUrl;
finalUrl.href = result.finalUrl;
statusMessage.textContent = "Website check completed.";
statusMessage.textContent =
"Website check completed.";
resultPanel.hidden = false;
} catch (error) {
statusMessage.textContent = error.message;
@@ -97,36 +126,52 @@ uptimeForm.addEventListener("submit", async (event) => {
// ---------------------------------------------------------
const dnsForm = document.getElementById("dns-form");
const hostnameInput = document.getElementById("hostname-input");
const hostnameInput = document.getElementById(
"hostname-input"
);
const dnsButton = document.getElementById("dns-button");
const dnsMessage = document.getElementById("dns-message");
const dnsResultPanel = document.getElementById("dns-result-panel");
const dnsHostname = document.getElementById("dns-hostname");
const dnsResultPanel = document.getElementById(
"dns-result-panel"
);
const dnsHostname = document.getElementById(
"dns-hostname"
);
const dnsRecords = document.getElementById("dns-records");
const dnsCheckedAt = document.getElementById("dns-checked-at");
const dnsCheckedAt = document.getElementById(
"dns-checked-at"
);
// Convert one DNS record into readable text.
function formatDnsRecord(recordType, record) {
if (recordType === "MX" && typeof record === "object") {
// MX records are objects containing an exchange and priority.
if (
recordType === "MX" &&
typeof record === "object" &&
record !== null
) {
return `${record.exchange} (priority ${record.priority})`;
}
return String(record);
}
// Build one visible record group.
// Build one DNS record section.
function createDnsRecordGroup(recordType, records) {
const group = document.createElement("section");
group.className = "dns-record-group";
const heading = document.createElement("h4");
heading.textContent = recordType;
group.appendChild(heading);
// Show an empty state when no records exist.
if (!Array.isArray(records) || records.length === 0) {
const emptyMessage = document.createElement("p");
emptyMessage.className = "empty-record";
emptyMessage.textContent = "No records found.";
group.appendChild(emptyMessage);
return group;
@@ -137,7 +182,12 @@ function createDnsRecordGroup(recordType, records) {
records.forEach((record) => {
const item = document.createElement("li");
item.textContent = formatDnsRecord(recordType, record);
item.textContent = formatDnsRecord(
recordType,
record
);
list.appendChild(item);
});
@@ -146,37 +196,54 @@ function createDnsRecordGroup(recordType, records) {
return group;
}
// Handle DNS form submissions.
dnsForm.addEventListener("submit", async (event) => {
event.preventDefault();
const hostname = hostnameInput.value.trim();
// Reset the DNS interface before the request.
dnsMessage.textContent = "Looking up DNS records...";
dnsMessage.className = "status-message";
dnsResultPanel.hidden = true;
dnsButton.disabled = true;
dnsButton.textContent = "Looking Up...";
try {
const response = await fetch("/api/dns", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ hostname }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || "The DNS lookup failed.");
throw new Error(
result.error || "The DNS lookup failed."
);
}
dnsHostname.textContent = result.hostname;
// Remove any records from the previous lookup.
dnsRecords.replaceChildren();
const recordOrder = ["A", "AAAA", "CNAME", "MX", "TXT", "NS"];
const recordOrder = [
"A",
"AAAA",
"CNAME",
"MX",
"TXT",
"NS",
];
// Render each supported record type.
recordOrder.forEach((recordType) => {
const group = createDnsRecordGroup(
recordType,
@@ -187,7 +254,9 @@ dnsForm.addEventListener("submit", async (event) => {
});
dnsCheckedAt.textContent =
`Checked ${new Date(result.checkedAt).toLocaleString()}`;
`Checked ${new Date(
result.checkedAt
).toLocaleString()}`;
dnsMessage.textContent = "DNS lookup completed.";
dnsResultPanel.hidden = false;
@@ -199,11 +268,12 @@ dnsForm.addEventListener("submit", async (event) => {
dnsButton.textContent = "Look Up DNS";
}
});
// ---------------------------------------------------------
// SSL Certificate Checker
// ---------------------------------------------------------
// Get references to the SSL form and result elements.
const sslForm = document.getElementById("ssl-form");
const sslHostnameInput = document.getElementById(
"ssl-hostname-input"
@@ -217,16 +287,22 @@ const sslResultPanel = document.getElementById(
const sslStatusBadge = document.getElementById(
"ssl-status-badge"
);
const sslHostname = document.getElementById("ssl-hostname");
const sslHostname = document.getElementById(
"ssl-hostname"
);
const sslDaysRemaining = document.getElementById(
"ssl-days-remaining"
);
const sslValidFrom = document.getElementById("ssl-valid-from");
const sslValidFrom = document.getElementById(
"ssl-valid-from"
);
const sslValidTo = document.getElementById("ssl-valid-to");
const sslProtocol = document.getElementById("ssl-protocol");
const sslCipher = document.getElementById("ssl-cipher");
const sslAddress = document.getElementById("ssl-address");
const sslCheckedAt = document.getElementById("ssl-checked-at");
const sslCheckedAt = document.getElementById(
"ssl-checked-at"
);
const sslSubject = document.getElementById("ssl-subject");
const sslIssuer = document.getElementById("ssl-issuer");
@@ -234,14 +310,16 @@ const sslFingerprint = document.getElementById(
"ssl-fingerprint"
);
const sslAuthorizationErrorBlock = document.getElementById(
"ssl-authorization-error-block"
);
const sslAuthorizationErrorBlock =
document.getElementById(
"ssl-authorization-error-block"
);
const sslAuthorizationError = document.getElementById(
"ssl-authorization-error"
);
// Convert certificate date strings into a readable local date.
// Convert a certificate date into local readable text.
function formatCertificateDate(value) {
if (!value) {
return "Unknown";
@@ -256,9 +334,12 @@ function formatCertificateDate(value) {
return date.toLocaleString();
}
// Choose readable text for the certificate expiration state.
// Convert days remaining into readable text.
function getExpirationText(daysRemaining) {
if (daysRemaining === null || daysRemaining === undefined) {
if (
daysRemaining === null ||
daysRemaining === undefined
) {
return "Unknown";
}
@@ -279,19 +360,22 @@ sslForm.addEventListener("submit", async (event) => {
const hostname = sslHostnameInput.value.trim();
// Reset the SSL interface before beginning the request.
// Reset the SSL interface before the request.
sslMessage.textContent = "Checking certificate...";
sslMessage.className = "status-message";
sslResultPanel.hidden = true;
sslButton.disabled = true;
sslButton.textContent = "Checking...";
try {
const response = await fetch("/api/ssl", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ hostname }),
});
@@ -299,53 +383,60 @@ sslForm.addEventListener("submit", async (event) => {
if (!response.ok) {
throw new Error(
result.error || "The SSL certificate check failed."
result.error ||
"The SSL certificate check failed."
);
}
// Show whether Node trusted the certificate chain and hostname.
// Show whether Node trusted the certificate.
if (result.authorized) {
sslStatusBadge.textContent = "Valid";
sslStatusBadge.className = "status-badge online";
sslStatusBadge.className =
"status-badge online";
} else {
sslStatusBadge.textContent = "Not trusted";
sslStatusBadge.className = "status-badge offline";
sslStatusBadge.className =
"status-badge offline";
}
// Populate the certificate summary.
sslHostname.textContent = result.hostname;
sslDaysRemaining.textContent = getExpirationText(
result.daysRemaining
);
sslValidFrom.textContent = formatCertificateDate(
result.validFrom
);
sslValidTo.textContent = formatCertificateDate(
result.validTo
);
sslDaysRemaining.textContent =
getExpirationText(result.daysRemaining);
sslProtocol.textContent = result.protocol || "Unknown";
sslCipher.textContent = result.cipher || "Unknown";
sslValidFrom.textContent =
formatCertificateDate(result.validFrom);
sslValidTo.textContent =
formatCertificateDate(result.validTo);
sslProtocol.textContent =
result.protocol || "Unknown";
sslCipher.textContent =
result.cipher || "Unknown";
sslAddress.textContent = result.resolvedAddress
? `${result.resolvedAddress} (IPv${result.addressFamily})`
: "Unknown";
sslCheckedAt.textContent = formatCertificateDate(
result.checkedAt
);
sslCheckedAt.textContent =
formatCertificateDate(result.checkedAt);
sslSubject.textContent =
result.subject || "Unknown";
sslIssuer.textContent =
result.issuer || "Unknown";
// Populate the longer certificate values.
sslSubject.textContent = result.subject || "Unknown";
sslIssuer.textContent = result.issuer || "Unknown";
sslFingerprint.textContent =
result.fingerprint256 || "Unavailable";
// Show the authorization problem only when one exists.
// Only show the authorization error when one exists.
if (result.authorizationError) {
sslAuthorizationError.textContent =
result.authorizationError;
sslAuthorizationErrorBlock.hidden = false;
} else {
sslAuthorizationError.textContent = "";
@@ -354,6 +445,7 @@ sslForm.addEventListener("submit", async (event) => {
sslMessage.textContent =
"Certificate check completed.";
sslResultPanel.hidden = false;
} catch (error) {
sslMessage.textContent = error.message;
@@ -362,4 +454,208 @@ sslForm.addEventListener("submit", async (event) => {
sslButton.disabled = false;
sslButton.textContent = "Check Certificate";
}
});
// ---------------------------------------------------------
// HTTP Headers Checker
// ---------------------------------------------------------
const headersForm = document.getElementById(
"headers-form"
);
const headersUrlInput = document.getElementById(
"headers-url-input"
);
const headersButton = document.getElementById(
"headers-button"
);
const headersMessage = document.getElementById(
"headers-message"
);
const headersResultPanel = document.getElementById(
"headers-result-panel"
);
const headersScoreBadge = document.getElementById(
"headers-score-badge"
);
const headersStatus = document.getElementById(
"headers-status"
);
const headersRedirects = document.getElementById(
"headers-redirects"
);
const headersScore = document.getElementById(
"headers-score"
);
const headersCheckedAt = document.getElementById(
"headers-checked-at"
);
const headersFinalUrl = document.getElementById(
"headers-final-url"
);
const securityHeaderResults = document.getElementById(
"security-header-results"
);
const allHeaderResults = document.getElementById(
"all-header-results"
);
// Build one visible security-header result.
function createSecurityHeaderItem(header) {
const item = document.createElement("article");
item.className = "security-header-item";
const heading = document.createElement("div");
heading.className = "security-header-heading";
const name = document.createElement("strong");
name.textContent = header.name;
const status = document.createElement("span");
status.className = header.present
? "header-status present"
: "header-status missing";
status.textContent = header.present
? "Present"
: "Missing";
heading.append(name, status);
const purpose = document.createElement("p");
purpose.className = "security-header-purpose";
purpose.textContent = header.purpose;
item.append(heading, purpose);
// Show the raw header value when present.
if (header.present && header.value) {
const value = document.createElement("p");
value.className = "security-header-value";
const code = document.createElement("code");
code.textContent = header.value;
value.appendChild(code);
item.appendChild(value);
}
return item;
}
// Build one raw response-header row.
function createResponseHeaderItem(name, value) {
const item = document.createElement("div");
item.className = "response-header-item";
const nameElement = document.createElement("div");
nameElement.className = "response-header-name";
nameElement.textContent = name;
const valueElement = document.createElement("div");
valueElement.className = "response-header-value";
valueElement.textContent = value;
item.append(nameElement, valueElement);
return item;
}
// Handle HTTP header form submissions.
headersForm.addEventListener("submit", async (event) => {
event.preventDefault();
const url = headersUrlInput.value.trim();
// Reset the interface before checking.
headersMessage.textContent =
"Checking response headers...";
headersMessage.className = "status-message";
headersResultPanel.hidden = true;
headersButton.disabled = true;
headersButton.textContent = "Checking...";
try {
const response = await fetch("/api/headers", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ url }),
});
const result = await response.json();
if (!response.ok) {
throw new Error(
result.error ||
"The HTTP header check failed."
);
}
// Fill in the summary information.
headersStatus.textContent =
`${result.statusCode} ${
result.statusText || ""
}`.trim();
headersRedirects.textContent =
result.redirectCount;
headersScore.textContent =
`${result.presentSecurityHeaderCount} of ` +
`${result.totalSecurityHeaderCount}`;
headersScoreBadge.textContent =
`${result.presentSecurityHeaderCount}/` +
`${result.totalSecurityHeaderCount}`;
headersCheckedAt.textContent =
new Date(result.checkedAt).toLocaleString();
headersFinalUrl.textContent = result.finalUrl;
headersFinalUrl.href = result.finalUrl;
// Remove old security header results.
securityHeaderResults.replaceChildren();
result.securityHeaders.forEach((header) => {
securityHeaderResults.appendChild(
createSecurityHeaderItem(header)
);
});
// Remove old raw response headers.
allHeaderResults.replaceChildren();
// Render all response headers alphabetically.
Object.entries(result.headers)
.sort(([nameA], [nameB]) =>
nameA.localeCompare(nameB)
)
.forEach(([name, value]) => {
allHeaderResults.appendChild(
createResponseHeaderItem(name, value)
);
});
headersMessage.textContent =
"Header check completed.";
headersResultPanel.hidden = false;
} catch (error) {
headersMessage.textContent = error.message;
headersMessage.className = "status-message error";
} finally {
headersButton.disabled = false;
headersButton.textContent = "Check Headers";
}
});
+142 -7
View File
@@ -15,10 +15,12 @@
<main class="page-shell">
<header class="site-header">
<p class="eyebrow">BYTEGEIST TOOLBOX</p>
<h1>Practical Website and Network Tools</h1>
<p class="description">
Check a website's current status, inspect its public DNS records,
or review its SSL certificate.
review its SSL certificate, or analyze its HTTP response headers.
</p>
</header>
@@ -49,12 +51,22 @@
>
SSL Certificate
</button>
<button
id="headers-tab"
class="tool-tab"
type="button"
data-tool="headers-tool"
>
HTTP Headers
</button>
</nav>
<!-- Website Status Checker -->
<section id="status-tool" class="tool-panel">
<header class="tool-header">
<h2>Website Status Checker</h2>
<p class="tool-description">
Check whether a public website is responding right now and view its
status code, response time, redirects, and final destination.
@@ -80,12 +92,20 @@
</div>
</form>
<p id="status-message" class="status-message" aria-live="polite"></p>
<p
id="status-message"
class="status-message"
aria-live="polite"
></p>
<section id="result-panel" class="result-panel" hidden>
<div class="result-heading">
<h3>Current Status</h3>
<span id="online-badge" class="status-badge"></span>
<span
id="online-badge"
class="status-badge"
></span>
</div>
<dl class="result-grid">
@@ -112,6 +132,7 @@
<div class="final-url-block">
<span>Final URL</span>
<a
id="final-url"
href="#"
@@ -126,6 +147,7 @@
<section id="dns-tool" class="tool-panel" hidden>
<header class="tool-header">
<h2>DNS Lookup</h2>
<p class="tool-description">
Look up public A, AAAA, CNAME, MX, TXT, and NS records for a domain.
</p>
@@ -150,17 +172,31 @@
</div>
</form>
<p id="dns-message" class="status-message" aria-live="polite"></p>
<p
id="dns-message"
class="status-message"
aria-live="polite"
></p>
<section id="dns-result-panel" class="result-panel" hidden>
<div class="result-heading">
<h3>DNS Records</h3>
<span id="dns-hostname" class="hostname-badge"></span>
<span
id="dns-hostname"
class="hostname-badge"
></span>
</div>
<div id="dns-records" class="dns-records"></div>
<div
id="dns-records"
class="dns-records"
></div>
<p id="dns-checked-at" class="dns-checked-at"></p>
<p
id="dns-checked-at"
class="dns-checked-at"
></p>
</section>
</section>
@@ -277,6 +313,105 @@
</div>
</section>
</section>
<!-- HTTP Headers Checker -->
<section id="headers-tool" class="tool-panel" hidden>
<header class="tool-header">
<h2>HTTP Headers Checker</h2>
<p class="tool-description">
Inspect a website's response headers and see which common security
headers are present or missing.
</p>
</header>
<form id="headers-form" class="tool-form">
<label for="headers-url-input">Website URL</label>
<div class="input-row">
<input
id="headers-url-input"
name="url"
type="url"
placeholder="https://example.com"
autocomplete="url"
required
>
<button id="headers-button" type="submit">
Check Headers
</button>
</div>
</form>
<p
id="headers-message"
class="status-message"
aria-live="polite"
></p>
<section id="headers-result-panel" class="result-panel" hidden>
<div class="result-heading">
<h3>Header Results</h3>
<span
id="headers-score-badge"
class="hostname-badge"
></span>
</div>
<dl class="result-grid">
<div>
<dt>Status</dt>
<dd id="headers-status">-</dd>
</div>
<div>
<dt>Redirects</dt>
<dd id="headers-redirects">-</dd>
</div>
<div>
<dt>Security headers</dt>
<dd id="headers-score">-</dd>
</div>
<div>
<dt>Checked at</dt>
<dd id="headers-checked-at">-</dd>
</div>
</dl>
<div class="final-url-block">
<span>Final URL</span>
<a
id="headers-final-url"
href="#"
target="_blank"
rel="noopener noreferrer"
></a>
</div>
<section class="headers-section">
<h4>Security Header Review</h4>
<div
id="security-header-results"
class="security-header-results"
></div>
</section>
<section class="headers-section">
<h4>All Response Headers</h4>
<div
id="all-header-results"
class="all-header-results"
></div>
</section>
</section>
</section>
</main>
<!-- Load browser JavaScript after the page elements exist. -->
+297 -111
View File
@@ -18,12 +18,24 @@
--danger: #ff6b73;
}
/* Always respect the HTML hidden attribute. */
[hidden] {
display: none !important;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at top, #17253b 0%, var(--background) 42%);
radial-gradient(
circle at top,
#17253b 0%,
var(--background) 42%
);
color: var(--text);
font-family:
Inter,
ui-sans-serif,
@@ -39,24 +51,29 @@ input {
font: inherit;
}
button {
cursor: pointer;
}
/* Main page layout. */
.page-shell {
display: grid;
place-items: center;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 32px 20px;
}
.tool-panel {
/* Main site heading area. */
.site-header {
width: min(760px, 100%);
padding: 32px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(17, 24, 39, 0.96);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 8px;
color: var(--accent);
font-size: 0.78rem;
font-weight: 700;
@@ -65,26 +82,91 @@ input {
h1,
h2,
h3,
h4,
p {
margin-top: 0;
}
h1 {
margin-bottom: 12px;
font-size: clamp(2rem, 5vw, 3.25rem);
line-height: 1.05;
}
.description {
max-width: 62ch;
margin-bottom: 28px;
margin-bottom: 0;
color: var(--muted);
line-height: 1.6;
}
.uptime-form label {
/* Tool navigation tabs. */
.tool-tabs {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
width: min(760px, 100%);
margin-bottom: 14px;
}
.tool-tab {
padding: 10px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1523;
color: var(--muted);
font-weight: 700;
}
.tool-tab:hover {
background: var(--surface-light);
color: var(--text);
}
.tool-tab.active {
border-color: var(--accent);
background: rgba(76, 166, 255, 0.12);
color: var(--accent);
}
/* Shared tool panel layout. */
.tool-panel {
width: min(760px, 100%);
padding: 32px;
border: 1px solid var(--border);
border-radius: 14px;
background: rgba(17, 24, 39, 0.96);
box-shadow:
0 20px 60px rgba(0, 0, 0, 0.35);
}
.tool-header h2 {
margin-bottom: 8px;
font-size: 1.6rem;
}
.tool-description {
margin-bottom: 24px;
color: var(--muted);
line-height: 1.6;
}
/* Shared form styles. */
.tool-form label {
display: block;
margin-bottom: 8px;
font-weight: 650;
}
@@ -97,29 +179,36 @@ h1 {
input {
min-width: 0;
padding: 13px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #0b1220;
color: var(--text);
outline: none;
}
input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(76, 166, 255, 0.16);
box-shadow:
0 0 0 3px rgba(76, 166, 255, 0.16);
}
button {
button[type="submit"] {
padding: 13px 18px;
border: 0;
border-radius: 8px;
background: var(--accent);
color: #04101c;
font-weight: 750;
cursor: pointer;
}
button:hover {
button[type="submit"]:hover {
background: var(--accent-hover);
}
@@ -128,9 +217,11 @@ button:disabled {
opacity: 0.65;
}
/* Loading, success, and error message area. */
.status-message {
min-height: 24px;
margin: 18px 0 0;
color: var(--muted);
}
@@ -138,11 +229,14 @@ button:disabled {
color: var(--danger);
}
/* Shared result panel. */
.result-panel {
margin-top: 24px;
padding: 22px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--surface-light);
}
@@ -151,17 +245,22 @@ button:disabled {
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.result-heading h2 {
.result-heading h3 {
margin-bottom: 0;
font-size: 1.25rem;
}
.status-badge {
/* Shared status badges. */
.status-badge,
.hostname-badge {
padding: 5px 10px;
border-radius: 999px;
font-size: 0.82rem;
font-weight: 750;
}
@@ -176,39 +275,58 @@ button:disabled {
color: var(--danger);
}
.hostname-badge {
background: rgba(76, 166, 255, 0.12);
color: var(--accent);
overflow-wrap: anywhere;
}
/* Shared result grid. */
.result-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin: 0;
}
.result-grid div {
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1523;
}
.result-grid dt,
.final-url-block span {
.final-url-block span,
.certificate-detail-block span {
margin-bottom: 6px;
color: var(--muted);
font-size: 0.82rem;
}
.result-grid dd {
margin: 0;
font-weight: 700;
overflow-wrap: anywhere;
}
/* Final URL display. */
.final-url-block {
display: grid;
gap: 5px;
margin-top: 14px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1523;
}
@@ -217,78 +335,7 @@ button:disabled {
overflow-wrap: anywhere;
}
@media (max-width: 640px) {
.tool-panel {
padding: 24px 18px;
}
.input-row {
grid-template-columns: 1fr;
}
.result-grid {
grid-template-columns: 1fr;
}
button {
width: 100%;
}
}
.site-header {
width: min(760px, 100%);
margin-bottom: 18px;
}
.site-header h1 {
margin-bottom: 10px;
}
.tool-tabs {
display: flex;
width: min(760px, 100%);
gap: 8px;
margin-bottom: 14px;
}
.tool-tab {
padding: 10px 14px;
border: 1px solid var(--border);
background: #0d1523;
color: var(--muted);
}
.tool-tab:hover {
background: var(--surface-light);
color: var(--text);
}
.tool-tab.active {
border-color: var(--accent);
background: rgba(76, 166, 255, 0.12);
color: var(--accent);
}
.tool-header h2 {
margin-bottom: 8px;
font-size: 1.6rem;
}
.tool-description {
margin-bottom: 24px;
color: var(--muted);
line-height: 1.6;
}
.hostname-badge {
padding: 5px 10px;
border-radius: 999px;
background: rgba(76, 166, 255, 0.12);
color: var(--accent);
font-size: 0.82rem;
font-weight: 700;
overflow-wrap: anywhere;
}
/* DNS result styles. */
.dns-records {
display: grid;
gap: 12px;
@@ -297,6 +344,7 @@ button:disabled {
.dns-record-group {
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1523;
overflow: hidden;
}
@@ -304,7 +352,9 @@ button:disabled {
.dns-record-group h4 {
margin: 0;
padding: 11px 14px;
border-bottom: 1px solid var(--border);
color: var(--accent);
font-size: 0.9rem;
letter-spacing: 0.08em;
@@ -323,39 +373,29 @@ button:disabled {
.empty-record {
margin: 0;
padding: 12px 14px;
color: var(--muted);
}
.dns-checked-at {
margin: 14px 0 0;
color: var(--muted);
font-size: 0.84rem;
}
@media (max-width: 640px) {
.tool-tabs {
display: grid;
grid-template-columns: 1fr 1fr;
}
.tool-tab {
width: 100%;
}
}
/* SSL certificate result styles. */
.certificate-detail-block {
display: grid;
gap: 6px;
margin-top: 14px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1523;
}
.certificate-detail-block span {
color: var(--muted);
font-size: 0.82rem;
background: #0d1523;
}
.certificate-detail-block p {
@@ -365,10 +405,12 @@ button:disabled {
.certificate-detail-block code {
color: var(--accent);
font-family:
"Cascadia Code",
"Courier New",
monospace;
line-height: 1.5;
overflow-wrap: anywhere;
white-space: normal;
@@ -378,13 +420,157 @@ button:disabled {
border-color: rgba(255, 107, 115, 0.5);
}
@media (max-width: 640px) {
/* HTTP header result sections. */
.headers-section {
margin-top: 22px;
}
.headers-section h4 {
margin: 0 0 12px;
font-size: 1rem;
}
.security-header-results,
.all-header-results {
display: grid;
gap: 10px;
}
/* One security header result card. */
.security-header-item {
display: grid;
gap: 7px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1523;
}
.security-header-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.security-header-heading strong {
overflow-wrap: anywhere;
}
.security-header-purpose,
.security-header-value {
margin: 0;
color: var(--muted);
line-height: 1.5;
overflow-wrap: anywhere;
}
.security-header-value code {
color: var(--accent);
white-space: normal;
}
/* Present or missing security header badge. */
.header-status {
flex-shrink: 0;
padding: 4px 8px;
border-radius: 999px;
font-size: 0.76rem;
font-weight: 750;
}
.header-status.present {
background: rgba(73, 209, 125, 0.14);
color: var(--success);
}
.header-status.missing {
background: rgba(255, 107, 115, 0.14);
color: var(--danger);
}
/* One raw response header row. */
.response-header-item {
display: grid;
grid-template-columns: minmax(150px, 220px) minmax(0, 1fr);
gap: 16px;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1523;
}
.response-header-name {
color: var(--accent);
font-family:
"Cascadia Code",
"Courier New",
monospace;
font-weight: 700;
overflow-wrap: anywhere;
}
.response-header-value {
color: var(--text);
overflow-wrap: anywhere;
}
/* Tablet layout. */
@media (max-width: 780px) {
.tool-tabs {
grid-template-columns: 1fr;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Always hide elements that use the HTML hidden attribute. */
[hidden] {
display: none !important;
/* Phone layout. */
@media (max-width: 640px) {
.page-shell {
padding: 24px 14px;
}
.tool-panel {
padding: 24px 18px;
}
.tool-tabs {
grid-template-columns: 1fr;
}
.tool-tab {
width: 100%;
}
.input-row {
grid-template-columns: 1fr;
}
.result-grid {
grid-template-columns: 1fr;
}
button[type="submit"] {
width: 100%;
}
.result-heading {
align-items: flex-start;
flex-direction: column;
}
.response-header-item {
grid-template-columns: 1fr;
gap: 6px;
}
}