mirror of
https://github.com/agessaman/meshcore-bot.git
synced 2026-11-04 11:57:16 +00:00
On a fresh install the dashboard was about fifteen identical "No data." boxes and empty donut rings, which read as broken at exactly the moment an operator wants to know the new bot is working. Real-time said "Nothing heard yet" forever, with no sense of whether the silence was normal. Until the snapshot has seen any node or packet, the dashboard now shows one panel in place of the grid: - What the station is doing, from the same verdict as the strip: listening, starting, not running (start the bot), or the radio problem in the way. - What fills in first: nodes from adverts, then paths and hop counts, then one-hop neighbors. - That timing depends on the mesh. - For admins, buttons to watch Real-time and check the radio settings; both pages are admin-only, so anonymous visitors get the panel without them. The grid comes back on its own once anything is heard. Real-time's empty tape now says "Listening since 2:28 PM (12 min) · nothing heard yet", counting each second from the bot's start time, which /api/banner-status now reports while the bot runs. After ten minutes with nothing, it adds one line of diagnosis: that can be normal on a quiet mesh, otherwise check the frequency and preset in Radio settings, or the Logs. When the bot isn't running or the radio can't receive, it says that instead.
504 lines
11 KiB
CSS
504 lines
11 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;
|
|
}
|
|
|
|
/* ── First run ─────────────────────────────────────────────────────────── */
|
|
/* Shown instead of the grid until the station has heard anything; one panel
|
|
in the station's own voice rather than fifteen "No data." boxes. */
|
|
|
|
.first-run {
|
|
max-width: 46rem;
|
|
padding: 1.25rem 1.5rem;
|
|
background-color: var(--card-bg);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 10px;
|
|
box-shadow: 0 1px 3px var(--shadow-color);
|
|
}
|
|
|
|
.first-run__title {
|
|
margin: 0 0 0.35rem;
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.first-run__state {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.first-run__state .status-indicator {
|
|
flex: none;
|
|
margin-right: 0;
|
|
}
|
|
|
|
.first-run__lead,
|
|
.first-run__note {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.first-run__lead {
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.first-run__steps {
|
|
margin: 0 0 1rem;
|
|
padding-left: 1.25rem;
|
|
}
|
|
|
|
.first-run__steps li {
|
|
margin-bottom: 0.4rem;
|
|
padding-left: 0.25rem;
|
|
}
|
|
|
|
.first-run__steps li::marker {
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.first-run__actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
/* ── 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;
|
|
}
|
|
|
|
/* Hatched, not empty: "we have no reading" reads differently from "zero". */
|
|
.neighbor-row__track--unknown {
|
|
background-image: repeating-linear-gradient(
|
|
45deg,
|
|
var(--bg-tertiary),
|
|
var(--bg-tertiary) 3px,
|
|
transparent 3px,
|
|
transparent 6px
|
|
);
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* ── 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;
|
|
}
|
|
|
|
.top-list-row__rank {
|
|
flex: 0 0 1.5rem;
|
|
color: var(--text-muted);
|
|
font-variant-numeric: tabular-nums;
|
|
text-align: right;
|
|
}
|
|
|
|
.top-list-row .tag-chip {
|
|
flex-shrink: 0;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
/* ── Chart wrappers ────────────────────────────────────────────────────── */
|
|
|
|
.chart-box {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 200px;
|
|
}
|
|
|
|
.chart-box--tall {
|
|
height: 240px;
|
|
}
|
|
|
|
/* Cards in an equal-height row: let the chart or list take the slack instead
|
|
* of leaving it as dead space under a fixed-height canvas. Chart.js needs a
|
|
* positioned parent with a real height, which flex-basis: 0 + flex-grow gives
|
|
* it — plain height: 100% resolves against an auto-height parent and collapses.
|
|
*/
|
|
.card-body--fill {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.chart-box--fill {
|
|
flex: 1 1 0;
|
|
min-height: 200px;
|
|
height: auto;
|
|
}
|
|
|
|
.mix-fill {
|
|
flex: 1 1 auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-evenly;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
@media (max-width: 575.98px) {
|
|
.stat-tile__value {
|
|
font-size: 1.35rem;
|
|
}
|
|
|
|
.health-strip {
|
|
gap: 0.25rem 1rem;
|
|
}
|
|
}
|
|
|
|
/* Touch screens: the bare icon buttons get a 36px hit area. Padding with a
|
|
matching negative margin grows the target without moving the icon. */
|
|
@media (pointer: coarse) {
|
|
.dashboard-info-btn,
|
|
.health-strip .btn-refresh {
|
|
padding: 0.7rem;
|
|
margin: -0.7rem;
|
|
}
|
|
}
|