From ac572e165a22f18c48a0da47612b7497c8ae3cb4 Mon Sep 17 00:00:00 2001 From: Daniel Redetzke Date: Mon, 5 Oct 2026 10:10:51 +0300 Subject: [PATCH] Health card: addresses beside a list of checks The public addresses stack on the left and the other checks are rows in one list on the right, each with its raw setting right after the status. Below 1000px the addresses move above the list. --- app.css | 32 ++++++++++++++++++-------------- app.js | 16 ++++++++-------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/app.css b/app.css index b914951..3976e1f 100644 --- a/app.css +++ b/app.css @@ -173,28 +173,32 @@ fieldset { border: 0; margin: 0; padding: 0; min-width: 0; display: flex; flex-d .kv dt { color: var(--ink-2); } .kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; } -/* health: public addresses, then one tile per check */ +/* health: public addresses on the left, one row per check on the right */ +.hcbody { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: 12px; margin-top: 16px; } .hchead { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; } .hchead > span { font-size: 13px; color: var(--ink-2); } .hchead > span.bad { color: var(--bad-ink); font-weight: 500; } -.hcaddrs { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; margin-top: 16px; } -.hcaddr { background: var(--ground); border-radius: 10px; padding: 14px 16px; min-width: 0; } +.hcaddrs { display: flex; flex-direction: column; gap: 12px; } +.hcaddr { flex: 1; display: flex; flex-direction: column; justify-content: center; background: var(--ground); border-radius: 10px; padding: 14px 18px; min-width: 0; } .hcaddr .l { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-2); } .hcaddr .v { margin-top: 4px; font-size: 15px; font-weight: 500; overflow-wrap: anywhere; } -.hcaddr .v.mono { font-size: 20px; } +.hcaddr .v.mono { font-size: 22px; } .hcaddr .n { font-family: var(--sans); font-size: 12px; font-weight: 400; color: var(--ink-2); } .hcaddr.bad { background: #fdf6f5; box-shadow: inset 0 0 0 1px #e6b3b0; } .hcaddr.bad .v { color: var(--bad-ink); } -.hctiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; margin-top: 12px; } -.hctile { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; min-width: 0; } -.hctile .l { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-2); } -.hctile .l > span:first-child { flex: 1; min-width: 0; } -.hctile .s { font-size: 15px; font-weight: 500; } -.hctile .r { font-size: 11.5px; color: var(--ink-3); overflow-wrap: anywhere; } -.hctile .p { font-size: 12.5px; color: var(--bad-ink); overflow-wrap: anywhere; } -.hctile.bad { border-color: #e6b3b0; background: #fdf6f5; } -.hctile.bad .s { color: var(--bad-ink); } -@media (max-width: 640px) { .hctiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } } +.hclist { border: 1px solid var(--line); border-radius: 10px; min-width: 0; } +.hcrow { display: grid; grid-template-columns: 8px minmax(0, 190px) minmax(0, 1fr); gap: 2px 14px; align-items: baseline; padding: 11px 16px; border-top: 1px solid var(--line-2); } +.hcrow:first-child { border-top: 0; } +.hcrow > .dot { align-self: center; } +.hcrow .l { font-size: 13px; color: var(--ink-2); } +.hcrow .v { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; min-width: 0; } +.hcrow .s { font-weight: 500; } +.hcrow .r { font-size: 12px; color: var(--ink-3); overflow-wrap: anywhere; } +.hcrow .p { grid-column: 2 / -1; font-size: 12.5px; color: var(--bad-ink); overflow-wrap: anywhere; } +.hcrow.bad { background: #fdf6f5; } +.hcrow.bad .s { color: var(--bad-ink); } +@media (max-width: 1000px) { .hcbody { grid-template-columns: minmax(0, 1fr); } } +@media (max-width: 640px) { .hcrow { grid-template-columns: 8px minmax(0, 1fr); } .hcrow .v { grid-column: 2; } } /* activity */ .ev { display: flex; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line-2); font-size: 13px; } diff --git a/app.js b/app.js index ebfcde5..f4994b4 100644 --- a/app.js +++ b/app.js @@ -1511,9 +1511,9 @@ const DNS_PRESETS = [['Quad9', '9.9.9.9, 149.112.112.112']]; // healthParts turns the server's checks into the Health card: the public - // address per IP family (from its uplink and public address checks), then - // one tile per other check with a plain-word status, the raw setting and, - // when it fails, what is wrong. + // address per IP family (from its uplink and public address checks) and, + // beside them, one row per other check with a plain-word status, the raw + // setting and, when it fails, what is wrong. function healthParts(checks) { const by = Object.fromEntries(checks.map((c) => [c.name, c])); const addrs = []; @@ -1563,14 +1563,14 @@ return h('section', { class: 'card', 'aria-labelledby': 'hc' }, h('div', { class: 'hchead' }, h('h2', { id: 'hc' }, 'Health'), h('span', { class: hp.failing ? 'bad' : null }, hp.failing ? hp.failing + ' of ' + hp.total + ' checks failing' : 'All ' + hp.total + ' checks pass')), + h('div', { class: 'hcbody' }, hp.addrs.length ? h('div', { class: 'hcaddrs' }, hp.addrs.map((a) => h('div', { class: a.ok ? 'hcaddr' : 'hcaddr bad' }, h('div', { class: 'l' }, dot(a.ok), a.label), h('div', { class: a.mono ? 'v mono' : 'v' }, a.value, a.note ? h('span', { class: 'n' }, ' ' + a.note) : null)))) : null, - h('div', { class: 'hctiles' }, hp.tiles.map((t) => h('div', { class: t.ok ? 'hctile' : 'hctile bad', title: t.title || null }, - h('div', { class: 'l' }, h('span', null, t.label), dot(t.ok)), - h('div', { class: 's' }, t.status), - t.raw ? h('div', { class: 'r mono' }, t.raw) : null, - t.problem ? h('div', { class: 'p' }, t.problem) : null)))); + h('div', { class: 'hclist' }, hp.tiles.map((t) => h('div', { class: t.ok ? 'hcrow' : 'hcrow bad', title: t.title || null }, + dot(t.ok), h('span', { class: 'l' }, t.label), + h('span', { class: 'v' }, h('span', { class: 's' }, t.status), t.raw ? h('span', { class: 'r mono' }, t.raw) : null), + t.problem ? h('div', { class: 'p' }, t.problem) : null))))); } async function viewServer(wrap) {