Files
meshcore-bot/modules/web_viewer/static/css/dashboard.css
T
agessaman c94a3f861e refactor(web-viewer): rework dashboard signal, role and contact tiles
Three adjustments from review.

Role and device type are the same field twice. Measured on the live
database they disagree on 16 of 11,028 contacts (ten roomservers and a
handful of bots and gateways reporting device Companion); every other
row is repeater/Repeater, companion/Companion, type11/Type11 and so on.
Charting both filled half a card with a copy of the other half. Keep the
role mix, which also carries the type0..type15 bucketing, and move it
into the routing row where the old signal card was.

Drop the tracked-contacts tile. is_currently_tracked does not describe
anything a reader can act on, and node activity is already covered by
nodes-heard and gone-quiet. The known-contacts total moves onto the
coverage tile, which leaves five tiles splitting the row evenly.

Rebuild the signal panel around zero-hop neighbours. Percentiles over
every received message answered no question anyone has: SNR on a relayed
packet measures the last hop into this radio, not the link to the node
that sent it, so averaging across hop counts describes nothing in
particular.

The panel now shows the nodes heard with no repeater in between — how
many, their SNR distribution, and the weakest links named, worst first,
on a fixed -12..+14 dB scale so bars mean the same thing between
refreshes. On the live database that is 307 neighbours, median 12.0 dB,
with eight marginal links surfaced from -9.0 dB down.

This also corrects the source. The plan rejected
complete_contact_tracking.snr as a badly biased 7% sample; in fact it is
populated on exactly the 800 hop_count=0 contacts and NULL on all 10,228
others. It is not a sample of the network, it is a complete census of
the neighbours — which is precisely the population the metric applies
to. message_stats.hops=0 covers only 34 senders by comparison.
2026-07-29 22:10:48 -07:00

425 lines
9.6 KiB
CSS

/* Dashboard (landing page) styles.
*
* Only the theme tokens declared in base.html are used (--bg-*, --text-*,
* --border-color, --card-*, --shadow-color), and every rule that hard-codes a
* colour has a matching [data-theme="dark"] override — Bootstrap 5.1.3 has no
* native dark theme here, so nothing inherits one for free.
*/
/* ── Health strip ──────────────────────────────────────────────────────── */
.health-strip {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.25rem 1.5rem;
min-height: 56px;
padding: 0.5rem 1rem;
margin-bottom: 1.25rem;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 10px;
box-shadow: 0 1px 3px var(--shadow-color);
}
.health-strip__item {
display: flex;
align-items: center;
gap: 0.45rem;
font-size: 0.875rem;
color: var(--text-color);
white-space: nowrap;
}
.health-strip__label {
color: var(--text-muted);
text-transform: uppercase;
font-size: 0.7rem;
letter-spacing: 0.04em;
}
.health-strip__spacer {
flex: 1 1 auto;
}
.health-strip .btn-refresh {
padding: 0.1rem 0.4rem;
line-height: 1;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
}
.health-strip .btn-refresh:hover,
.health-strip .btn-refresh:focus {
color: #0d6efd;
}
.health-strip .btn-refresh[disabled] {
opacity: 0.5;
cursor: progress;
}
/* ── Section headings ──────────────────────────────────────────────────── */
.dashboard-section-title {
display: flex;
align-items: baseline;
gap: 0.6rem;
margin: 1.75rem 0 0.75rem;
font-size: 0.8rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.dashboard-section-title::after {
content: "";
flex: 1 1 auto;
height: 1px;
background-color: var(--border-color);
}
/* ── Stat tiles ────────────────────────────────────────────────────────── */
.stat-tile {
display: flex;
flex-direction: column;
height: 100%;
padding: 0.75rem 0.9rem 0.5rem;
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 10px;
box-shadow: 0 1px 3px var(--shadow-color);
}
.stat-tile__label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
margin-bottom: 0.2rem;
}
.stat-tile__row {
display: flex;
align-items: baseline;
gap: 0.5rem;
flex-wrap: wrap;
}
.stat-tile__value {
font-size: 1.6rem;
font-weight: 700;
line-height: 1.1;
color: var(--text-color);
font-variant-numeric: tabular-nums;
}
.stat-tile__value--muted {
color: var(--text-muted);
}
.stat-tile__sub {
font-size: 0.75rem;
color: var(--text-muted);
}
.stat-tile__spark {
position: relative;
height: 48px;
margin-top: auto;
}
/* Sparkline canvases are sized by their container, not by their attributes. */
.stat-tile__spark canvas {
width: 100% !important;
height: 48px !important;
}
/* ── Delta chips ───────────────────────────────────────────────────────── */
.delta-chip {
display: inline-flex;
align-items: center;
gap: 0.15rem;
padding: 0.05rem 0.35rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
background-color: var(--bg-tertiary);
color: var(--text-muted);
}
.delta-chip--up {
background-color: rgba(25, 135, 84, 0.12);
color: #146c43;
}
.delta-chip--down {
background-color: rgba(220, 53, 69, 0.12);
color: #b02a37;
}
[data-theme="dark"] .delta-chip--up {
background-color: rgba(25, 135, 84, 0.22);
color: #75b798;
}
[data-theme="dark"] .delta-chip--down {
background-color: rgba(220, 53, 69, 0.22);
color: #ea868f;
}
/* ── Stacked route-mix bar ─────────────────────────────────────────────── */
.stacked-bar {
display: flex;
height: 26px;
width: 100%;
border-radius: 6px;
overflow: hidden;
background-color: var(--bg-tertiary);
}
.stacked-bar__segment {
display: flex;
align-items: center;
justify-content: center;
font-size: 0.72rem;
font-weight: 600;
color: #fff;
white-space: nowrap;
overflow: hidden;
min-width: 0;
}
.stacked-bar__legend {
display: flex;
flex-wrap: wrap;
gap: 0.25rem 1rem;
margin-top: 0.5rem;
font-size: 0.78rem;
color: var(--text-muted);
}
.stacked-bar__swatch {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 2px;
margin-right: 0.35rem;
vertical-align: baseline;
}
/* ── Horizontal mix rows (role / device) ───────────────────────────────── */
.mix-row {
display: grid;
grid-template-columns: minmax(5.5rem, 8rem) 1fr auto;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.35rem;
font-size: 0.82rem;
}
.mix-row__name {
color: var(--text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mix-row__track {
height: 10px;
border-radius: 5px;
background-color: var(--bg-tertiary);
overflow: hidden;
}
.mix-row__fill {
height: 100%;
border-radius: 5px;
}
.mix-row__count {
color: var(--text-muted);
font-variant-numeric: tabular-nums;
}
/* ── Direct-neighbour signal rows ──────────────────────────────────────── */
.neighbor-row {
display: grid;
grid-template-columns: minmax(6rem, 11rem) 1fr auto;
align-items: center;
gap: 0.6rem;
padding: 0.2rem 0;
font-size: 0.82rem;
}
.neighbor-row + .neighbor-row {
border-top: 1px solid var(--border-color);
}
.neighbor-row__name {
color: var(--text-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Fixed -12..+14 dB scale, so a bar means the same thing on every refresh. */
.neighbor-row__track {
height: 10px;
min-width: 3rem;
border-radius: 5px;
background-color: var(--bg-tertiary);
overflow: hidden;
}
.neighbor-row__fill {
height: 100%;
border-radius: 5px;
}
.neighbor-row__values {
color: var(--text-muted);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.neighbor-row__snr {
color: var(--text-color);
font-weight: 600;
}
/* ── Signal percentile readout ─────────────────────────────────────────── */
.signal-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
text-align: center;
}
.signal-grid__value {
font-size: 1.1rem;
font-weight: 700;
color: var(--text-color);
font-variant-numeric: tabular-nums;
}
.signal-grid__label {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--text-muted);
}
/* ── Leaderboard rows ──────────────────────────────────────────────────── */
.top-list-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0;
}
.top-list-row + .top-list-row {
border-top: 1px solid var(--border-color);
}
.top-list-row__name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* ── Chart wrappers ────────────────────────────────────────────────────── */
.chart-box {
position: relative;
width: 100%;
height: 200px;
}
.chart-box--tall {
height: 240px;
}
.doughnut-box {
position: relative;
max-width: 190px;
width: 100%;
margin: 0 auto;
}
.dashboard-note {
font-size: 0.75rem;
color: var(--text-muted);
}
.dashboard-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 100px;
color: var(--text-muted);
font-size: 0.85rem;
font-style: italic;
}
/* Info affordance shared by tooltip triggers in card headers. */
.dashboard-info-btn {
padding: 0;
border: 0;
background: none;
color: var(--text-muted);
line-height: 1;
}
.dashboard-info-btn:hover,
.dashboard-info-btn:focus {
color: #0dcaf0;
text-decoration: none;
}
.tooltip.dashboard-hint .tooltip-inner {
max-width: min(22rem, 92vw);
text-align: left;
}
/* ── Live activity feed ────────────────────────────────────────────────── */
.live-feed {
height: 260px;
overflow-y: auto;
background-color: var(--bg-secondary);
padding: 0.5rem;
}
.live-feed__entry {
padding: 3px 8px;
margin-bottom: 3px;
font-size: 0.82rem;
border-radius: 2px;
border-left: 3px solid var(--border-color);
}
@media (max-width: 575.98px) {
.stat-tile__value {
font-size: 1.35rem;
}
.health-strip {
gap: 0.25rem 1rem;
}
}