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.
This commit is contained in:
Daniel Redetzke
2026-10-05 10:10:51 +03:00
parent b18d16add1
commit 6662adf0b8
2 changed files with 26 additions and 22 deletions
+18 -14
View File
@@ -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 dt { color: var(--ink-2); }
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; } .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 { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.hchead > span { font-size: 13px; color: var(--ink-2); } .hchead > span { font-size: 13px; color: var(--ink-2); }
.hchead > span.bad { color: var(--bad-ink); font-weight: 500; } .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; } .hcaddrs { display: flex; flex-direction: column; gap: 12px; }
.hcaddr { background: var(--ground); border-radius: 10px; padding: 14px 16px; min-width: 0; } .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 .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 { 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 .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 { background: #fdf6f5; box-shadow: inset 0 0 0 1px #e6b3b0; }
.hcaddr.bad .v { color: var(--bad-ink); } .hcaddr.bad .v { color: var(--bad-ink); }
.hctiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; margin-top: 12px; } .hclist { border: 1px solid var(--line); border-radius: 10px; min-width: 0; }
.hctile { border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; display: flex; flex-direction: column; gap: 4px; 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); }
.hctile .l { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--ink-2); } .hcrow:first-child { border-top: 0; }
.hctile .l > span:first-child { flex: 1; min-width: 0; } .hcrow > .dot { align-self: center; }
.hctile .s { font-size: 15px; font-weight: 500; } .hcrow .l { font-size: 13px; color: var(--ink-2); }
.hctile .r { font-size: 11.5px; color: var(--ink-3); overflow-wrap: anywhere; } .hcrow .v { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 10px; min-width: 0; }
.hctile .p { font-size: 12.5px; color: var(--bad-ink); overflow-wrap: anywhere; } .hcrow .s { font-weight: 500; }
.hctile.bad { border-color: #e6b3b0; background: #fdf6f5; } .hcrow .r { font-size: 12px; color: var(--ink-3); overflow-wrap: anywhere; }
.hctile.bad .s { color: var(--bad-ink); } .hcrow .p { grid-column: 2 / -1; font-size: 12.5px; color: var(--bad-ink); overflow-wrap: anywhere; }
@media (max-width: 640px) { .hctiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } } .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 */ /* activity */
.ev { display: flex; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line-2); font-size: 13px; } .ev { display: flex; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--line-2); font-size: 13px; }
+8 -8
View File
@@ -1511,9 +1511,9 @@
const DNS_PRESETS = [['Quad9', '9.9.9.9, 149.112.112.112']]; const DNS_PRESETS = [['Quad9', '9.9.9.9, 149.112.112.112']];
// healthParts turns the server's checks into the Health card: the public // healthParts turns the server's checks into the Health card: the public
// address per IP family (from its uplink and public address checks), then // address per IP family (from its uplink and public address checks) and,
// one tile per other check with a plain-word status, the raw setting and, // beside them, one row per other check with a plain-word status, the raw
// when it fails, what is wrong. // setting and, when it fails, what is wrong.
function healthParts(checks) { function healthParts(checks) {
const by = Object.fromEntries(checks.map((c) => [c.name, c])); const by = Object.fromEntries(checks.map((c) => [c.name, c]));
const addrs = []; const addrs = [];
@@ -1563,14 +1563,14 @@
return h('section', { class: 'card', 'aria-labelledby': 'hc' }, return h('section', { class: 'card', 'aria-labelledby': 'hc' },
h('div', { class: 'hchead' }, h('h2', { id: 'hc' }, 'Health'), 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('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' }, 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: '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: 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: 'hclist' }, hp.tiles.map((t) => h('div', { class: t.ok ? 'hcrow' : 'hcrow bad', title: t.title || null },
h('div', { class: 'l' }, h('span', null, t.label), dot(t.ok)), dot(t.ok), h('span', { class: 'l' }, t.label),
h('div', { class: 's' }, t.status), h('span', { class: 'v' }, h('span', { class: 's' }, t.status), t.raw ? h('span', { class: 'r mono' }, t.raw) : null),
t.raw ? h('div', { class: 'r mono' }, t.raw) : null, t.problem ? h('div', { class: 'p' }, t.problem) : null)))));
t.problem ? h('div', { class: 'p' }, t.problem) : null))));
} }
async function viewServer(wrap) { async function viewServer(wrap) {