feat(monero-dashboard): add 90d history and rebuild the interface - #4394
Conversation
The logger kept one tier — 10s samples for 24h — so no range longer than a day could ever be drawn. Adds a 5-minute rollup retained for 90d (25920 points, the same order as the fine buffer) and a /stats_history.json endpoint that mean-aggregates either tier down to a fixed 720-point budget, so a 90d request costs the same on the wire as a 1h one. On first start the rollup is back-filled from the 24h log rather than showing three months of blank chart. Charts now read that endpoint through a 1h/6h/24h/7d/30d/90d control; the earnings and luck maths still use /stats_log.json unchanged. Interface rebuilt on OKLCH tokens: - muted text was #4b5563 on #0a0a0a, about 3:1 — every ink step now clears 4.5:1 - accent moved from red to Monero orange; red and green now mean state only, and the header pill reports whether the solar gate has the miner running - drops the per-card gradient stripe and the card-inside-a-card luck sections - replaces id-pinned 12-column placement with named breakpoints - loading skeletons in place of "Loading…", focus-visible rings, and a reduced-motion path Fixes two display bugs found while rendering: accumulated XMR printed 12 decimals (one atomic unit), and the all-time share counters repeated their own labels.
|
|
Overall Grade |
Security Reliability Complexity Hygiene |
Code Review Summary
| Analyzer | Status | Updated (UTC) | Details |
|---|---|---|---|
| JavaScript | Aug 9, 2026 10:29a.m. | Review ↗ | |
| Shell | Aug 9, 2026 10:29a.m. | Review ↗ |
Important
AI Review is run only on demand for your team. We're only showing results of static analysis review right now. To trigger AI Review, comment @deepsourcebot review on this thread.
@@ spec.values.persistence.dashboard-html.advancedMounts.dashboard.web @@
# helm.toolkit.fluxcd.io/v2/HelmRelease/web3/dashboard
! - three list entries removed:
- - path: /usr/share/nginx/html/index.html
- subPath: dashboard.html
- - path: /usr/share/nginx/html/dashboard.css
- subPath: dashboard.css
- - path: /usr/share/nginx/html/dashboard.js
- subPath: dashboard.js
! + one list entry added:
+ - path: /usr/share/nginx/html
@@ data.dashboard.css @@
# v1/ConfigMap/web3/dashboard
! ± value change in multiline text (100 inserts, 94 deletions)
/* =========================================================
- P2POOL DASHBOARD - INSTRUMENT PANEL THEME
- Black background, red accents, refined typography
+ P2POOL DASHBOARD
+ Dark instrument panel. One accent (Monero orange) for the
+ live series and current selection; red and green are
+ reserved for state, never decoration.
========================================================= */
- /* =========================================================
- RESET & BASE STYLING
- ========================================================= */
- * {
- margin: 0;
- padding: 0;
+ :root {
+ /* Neutrals carry a trace of the accent's hue rather than a default warm tint */
+ --bg: oklch(0.16 0.006 60);
+ --surface: oklch(0.2 0.007 60);
+ --surface-raised: oklch(0.245 0.008 60);
+ --line: oklch(0.3 0.01 60);
+ --line-strong: oklch(0.38 0.012 60);
+
+ /* Every ink step clears 4.5:1 on --surface */
+ --ink: oklch(0.97 0.004 60);
+ --ink-2: oklch(0.8 0.006 60);
+ --ink-3: oklch(0.66 0.008 60);
+
+ --accent: oklch(0.72 0.175 55);
+ --accent-dim: color-mix(in oklch, var(--accent) 14%, transparent);
+ --accent-line: color-mix(in oklch, var(--accent) 35%, transparent);
+ /* Chart.js reads these through getComputedStyle, so they live here too */
+ --chart-fill: color-mix(in oklch, var(--accent) 12%, transparent);
+ --chart-grid: color-mix(in oklch, var(--line) 60%, transparent);
+
+ --positive: oklch(0.76 0.15 155);
+ --positive-line: color-mix(in oklch, var(--positive) 40%, transparent);
+ --negative: oklch(0.68 0.19 25);
+
+ --font-ui:
+ system-ui, -apple-system, "Segoe UI", "Roboto", "Helvetica", "Arial",
+ sans-serif;
+ /* Figures are the content here: a mono keeps columns and deltas aligned */
+ --font-num:
+ ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", "Menlo",
+ "Consolas", monospace;
+
+ --step--2: 0.6875rem;
+ --step--1: 0.75rem;
+ --step-0: 0.8125rem;
+ --step-1: 0.875rem;
+ --step-2: 1rem;
+ --step-3: 1.25rem;
+ --step-4: 1.625rem;
+ --step-5: 2rem;
+
+ --gap: 1rem;
+ --pad: 1.125rem;
+ --radius: 10px;
+ --radius-sm: 6px;
+
+ --chart-h: 240px;
+ --chart-h-tall: 300px;
+
+ /* ease-out-quart: fast departure, no overshoot */
+ --ease: cubic-bezier(0.25, 1, 0.5, 1);
+ --speed: 160ms;
+
+ --z-sticky: 10;
+ }
+
+ *,
+ *::before,
+ *::after {
box-sizing: border-box;
}
body {
- font-family:
- -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
- background: #000000;
- color: #e5e7eb;
- overflow-x: hidden;
- font-weight: 400;
+ margin: 0;
+ background: var(--bg);
+ color: var(--ink-2);
+ font-family: var(--font-ui);
+ font-size: var(--step-0);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
+ h1,
+ h2,
+ h3,
+ h4,
+ p,
+ dl,
+ dd,
+ figure {
+ margin: 0;
+ }
+
+ :where(a, button, select, [tabindex]):focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+ border-radius: var(--radius-sm);
+ }
+
+ .visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+ }
+
/* =========================================================
- HEADER
+ MASTHEAD
========================================================= */
- header {
+ .masthead {
+ position: sticky;
+ top: 0;
+ z-index: var(--z-sticky);
display: flex;
- flex-direction: column;
+ flex-wrap: wrap;
align-items: center;
- padding: 1.5rem 1rem 0;
- border-bottom: 1px solid #1f2937;
- margin-bottom: 1.5rem;
+ justify-content: space-between;
+ gap: var(--gap);
+ padding: 0.875rem clamp(0.75rem, 3vw, 2rem);
+ background: var(--bg);
+ border-bottom: 1px solid var(--line);
}
- header h1 {
- margin: 0;
- color: #f9fafb;
- font-size: 1.5rem;
- font-weight: 300;
- letter-spacing: 2px;
- text-transform: uppercase;
+ .masthead h1 {
+ font-size: var(--step-3);
+ font-weight: 600;
+ letter-spacing: -0.01em;
+ color: var(--ink);
}
- .header-status {
+ .masthead__sub {
+ font-size: var(--step--1);
+ color: var(--ink-3);
+ }
+
+ .masthead__state {
display: flex;
align-items: center;
- gap: 0.5rem;
- font-size: 0.8rem;
- color: #6b7280;
- margin-top: 0.75rem;
- margin-bottom: 0.75rem;
+ gap: 0.75rem;
}
- .status-dot,
- .status-indicator {
- width: 8px;
- height: 8px;
+ .masthead__stamp {
+ font-family: var(--font-num);
+ font-size: var(--step--2);
+ color: var(--ink-3);
+ }
+
+ /* State pill: the one place colour means status rather than brand */
+ .pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.25rem 0.6rem 0.25rem 0.5rem;
+ border: 1px solid var(--line);
+ border-radius: 999px;
+ font-size: var(--step--1);
+ color: var(--ink-2);
+ white-space: nowrap;
+ }
+
+ .pill__dot,
+ .dot {
+ width: 7px;
+ height: 7px;
border-radius: 50%;
+ background: var(--ink-3);
+ flex: none;
}
- .status-indicator {
+ .dot {
display: inline-block;
- margin-right: 6px;
- vertical-align: middle;
+ margin-right: 0.4rem;
+ vertical-align: 0.05em;
}
- .status-active,
- .status-dot {
- background: #ef4444;
- box-shadow: 0 0 8px rgba(239, 68, 68, 0.5);
- animation: pulse 2s ease-in-out infinite;
+ .is-live .pill__dot,
+ .dot.is-live {
+ background: var(--positive);
+ animation: breathe 2.4s var(--ease) infinite;
}
- .status-inactive {
- background: #6b7280;
- box-shadow: none;
+ .is-down .pill__dot,
+ .dot.is-down {
+ background: var(--negative);
+ }
+
+ .is-live {
+ border-color: var(--positive-line);
}
- @keyframes pulse {
- 0%,
- 100% {
- opacity: 1;
- }
+ @keyframes breathe {
50% {
- opacity: 0.5;
+ opacity: 0.35;
}
}
/* =========================================================
- SMALL TEXT / LEGEND STYLING
+ PAGE SHELL
========================================================= */
- #earnLegend,
- #lastRefreshed {
- color: #6b7280;
- font-size: 0.75rem;
- margin-top: 0.25rem;
- letter-spacing: 0.5px;
+ main {
+ display: flex;
+ flex-direction: column;
+ gap: 1.75rem;
+ max-width: 100rem;
+ margin: 0 auto;
+ padding: 1.5rem clamp(0.75rem, 3vw, 2rem) 3rem;
}
- /* =========================================================
- TOOLTIP ICON STYLING
- ========================================================= */
- .tooltip-icon {
- display: inline-block;
- margin-left: 4px;
- color: #ef4444;
- font-weight: normal;
- font-size: 0.7rem;
- vertical-align: super;
- cursor: help;
- opacity: 0.7;
- transition: opacity 0.2s;
+ .section-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ margin-bottom: 0.75rem;
}
- .tooltip-icon:hover {
- opacity: 1;
+ .section-head h2 {
+ font-size: var(--step-2);
+ font-weight: 600;
+ color: var(--ink);
}
/* =========================================================
- MAIN GRID LAYOUT
+ OVERVIEW STRIP
========================================================= */
- main.grid {
+ .overview {
display: grid;
- grid-template-columns: repeat(12, 1fr);
- gap: 1.5rem;
- padding: 0 1.5rem 2rem;
- width: 100%;
- max-width: 150rem;
- margin: 0 auto;
- box-sizing: border-box;
+ grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
+ gap: 1px;
+ background: var(--line);
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ overflow: hidden;
}
- /* =========================================================
- CARD STYLING
- ========================================================= */
- .card {
- background: #0a0a0a;
- padding: 1.25rem;
- border-radius: 8px;
- border: 1px solid #1f2937;
+ .metric {
display: flex;
flex-direction: column;
- justify-content: space-between;
- box-sizing: border-box;
- max-width: 100%;
- position: relative;
- overflow: hidden;
- transition: border-color 0.2s;
+ gap: 0.25rem;
+ padding: var(--pad);
+ background: var(--surface);
}
- .card:hover {
- border-color: #374151;
+ .metric__label {
+ font-size: var(--step--1);
+ color: var(--ink-3);
}
- /* Red accent line at top of each card */
- .card::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- height: 2px;
- background: linear-gradient(90deg, transparent, #ef4444, transparent);
- opacity: 0.8;
+ .metric__value {
+ font-family: var(--font-num);
+ font-size: var(--step-5);
+ font-weight: 500;
+ line-height: 1.1;
+ /* Mono runs wide at display sizes; pull it back so "2.41 kH/s" reads as one figure */
+ letter-spacing: -0.03em;
+ color: var(--ink);
+ font-variant-numeric: tabular-nums;
}
- .card h2 {
- margin-top: 0;
- margin-bottom: 0.75rem;
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 1.5px;
- color: #6b7280;
- font-weight: 500;
+ .metric__value--sm {
+ font-size: var(--step-4);
}
- /* Large numeric values */
- .big {
- font-size: 1.75rem;
- font-weight: 200;
- margin: 0.5rem 0;
- color: #f9fafb;
+ .metric__note {
+ font-size: var(--step--1);
+ color: var(--ink-3);
font-variant-numeric: tabular-nums;
- line-height: 1.2;
}
- .big-med {
- font-size: 1.2rem;
- font-weight: 300;
- margin: 0.4rem 0;
- color: #e5e7eb;
+ /* =========================================================
+ RANGE CONTROL
+ ========================================================= */
+ .range {
+ display: inline-flex;
+ padding: 2px;
+ gap: 2px;
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ }
+
+ .range__opt {
+ appearance: none;
+ border: 0;
+ background: transparent;
+ color: var(--ink-3);
+ font: inherit;
+ font-size: var(--step--1);
font-variant-numeric: tabular-nums;
+ padding: 0.3rem 0.7rem;
+ border-radius: 4px;
+ cursor: pointer;
+ transition:
+ color var(--speed) var(--ease),
+ background-color var(--speed) var(--ease);
}
- /* Smaller info text */
- .sub,
- .small-stat,
- .stat {
- font-size: 0.8rem;
- margin: 0.25rem 0;
- color: #6b7280;
+ .range__opt:hover {
+ color: var(--ink);
+ background: var(--surface-raised);
}
- .sub-sub {
- font-size: 0.7rem;
- margin: 0.125rem 0;
- color: #4b5563;
+ .range__opt[aria-pressed="true"] {
+ background: var(--accent-dim);
+ color: var(--accent);
+ font-weight: 600;
}
/* =========================================================
- GRID POSITIONS
+ PANELS
========================================================= */
+ .trends__grid,
+ .panels {
+ display: grid;
+ gap: var(--gap);
+ grid-template-columns: 1fr;
+ }
- /* Top row - Charts take full width, split 50/50 */
- #hashrateCard {
- grid-column: 1 / 7;
- grid-row: 1;
+ .panel {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ padding: var(--pad);
+ background: var(--surface);
+ border: 1px solid var(--line);
+ border-radius: var(--radius);
+ min-width: 0;
+ transition: border-color var(--speed) var(--ease);
}
- #priceCard {
- grid-column: 7 / 13;
- grid-row: 1;
+
+ .panel:hover {
+ border-color: var(--line-strong);
}
- /* Second row */
- #paymentsCard {
- grid-column: 1 / 4;
- grid-row: 2;
+ .panel__head {
+ display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.75rem;
}
- #combinedLuckCard {
- grid-column: 4 / 9;
- grid-row: 2;
+
+ .panel__title {
+ font-size: var(--step-1);
+ font-weight: 600;
+ color: var(--ink);
}
- #earningsCard {
- grid-column: 9 / 11;
- grid-row: 2;
+
+ /* =========================================================
+ FIGURES & FACTS
+ ========================================================= */
+ .figure {
+ font-family: var(--font-num);
+ font-size: var(--step-4);
+ font-weight: 500;
+ line-height: 1.15;
+ color: var(--ink);
+ font-variant-numeric: tabular-nums;
}
- #sharesCard {
- grid-column: 11 / 13;
- grid-row: 2;
+
+ .figure--sm {
+ font-size: var(--step-3);
}
- /* Third row - Old dashboard stats */
- #poolStatusCard {
- grid-column: 1 / 8;
- grid-row: 3;
+ .figure__note {
+ font-size: var(--step-0);
+ color: var(--ink-2);
+ font-variant-numeric: tabular-nums;
}
- #workersCard {
- grid-column: 8 / 13;
- grid-row: 3;
+
+ .footnote {
+ font-size: var(--step--1);
+ color: var(--ink-3);
+ text-wrap: pretty;
}
- /* =========================================================
- COMBINED LUCK CARD
- ========================================================= */
- #combinedLuckCard .luck-grid {
+ .pair + .pair {
+ margin-top: 0.5rem;
+ }
+
+ .facts {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ margin-top: auto;
+ padding-top: 0.75rem;
+ }
+
+ .facts--grid {
display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 1rem;
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+ gap: 0.75rem 1.25rem;
}
- #combinedLuckCard .luck-section {
- background: #111827;
- padding: 0.75rem;
- border-radius: 6px;
+ .fact {
display: flex;
+ align-items: baseline;
+ justify-content: space-between;
+ gap: 0.75rem;
+ font-size: var(--step--1);
+ }
+
+ .facts--grid .fact {
flex-direction: column;
- gap: 0.3rem;
- border: 1px solid #1f2937;
+ align-items: flex-start;
+ gap: 0.15rem;
}
- #combinedLuckCard .luck-section h3 {
- margin: 0;
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 1px;
- color: #6b7280;
- font-weight: 500;
+ .fact dt {
+ color: var(--ink-3);
}
- /* =========================================================
- TRUE LUCK WINDOW LABEL
- ========================================================= */
- #trueLuckWindow {
- font-size: 0.75rem;
- color: #9ca3af;
- margin-top: 0.25rem;
- margin-bottom: 0.25rem;
+ .fact dd {
+ font-family: var(--font-num);
+ color: var(--ink-2);
font-variant-numeric: tabular-nums;
+ text-align: right;
}
- /* =========================================================
- CANVAS / CHART STYLING
- ========================================================= */
- .chart-container {
- position: relative;
- width: 100%;
- height: 200px;
- margin-top: 0.75rem;
+ .facts--grid .fact dd {
+ font-size: var(--step-0);
+ color: var(--ink);
+ text-align: left;
}
- canvas {
- border-radius: 4px;
+ /* Help affordance — a badge, not a superscript glyph */
+ .info {
+ display: inline-grid;
+ place-items: center;
+ width: 14px;
+ height: 14px;
+ margin-left: 0.3rem;
+ border: 1px solid var(--line-strong);
+ border-radius: 50%;
+ font-family: var(--font-ui);
+ font-size: 9px;
+ font-style: italic;
+ line-height: 1;
+ color: var(--ink-3);
+ cursor: help;
+ vertical-align: 0.15em;
+ transition:
+ color var(--speed) var(--ease),
+ border-color var(--speed) var(--ease);
+ }
+
+ .info:hover,
+ .info:focus-visible {
+ color: var(--accent);
+ border-color: var(--accent-line);
}
/* =========================================================
- DROPDOWN STYLING
+ CHARTS
========================================================= */
- select#earnPeriod {
- padding: 0.4rem 0.6rem;
- background: #111827;
- color: #e5e7eb;
- border: 1px solid #1f2937;
- border-radius: 4px;
- font-size: 0.8rem;
- margin-top: 0.5rem;
- margin-bottom: 0.5rem;
- appearance: none;
- cursor: pointer;
- font-variant-numeric: tabular-nums;
- transition: border-color 0.2s;
+ .chart {
+ position: relative;
+ width: 100%;
+ height: var(--chart-h);
+ margin-top: 0.25rem;
}
- #earnPeriod:hover {
- border-color: #374151;
+ .chart--tall {
+ height: var(--chart-h-tall);
}
- #earnPeriod:focus {
- outline: none;
- border-color: #ef4444;
- box-shadow: 0 0 0 2px rgba(239, 68, 68, 0.15);
+ /* An explicit `display` beats the UA rule for [hidden], so restore it */
+ [hidden] {
+ display: none !important;
}
- #earnPeriod::-ms-expand {
- display: none;
+ .chart__empty {
+ position: absolute;
+ inset: 0;
+ display: grid;
+ place-content: center;
+ text-align: center;
+ padding: 1.5rem;
+ max-width: 34ch;
+ margin: auto;
+ font-size: var(--step--1);
+ color: var(--ink-3);
+ text-wrap: pretty;
}
/* =========================================================
- EARNINGS CARD
+ LUCK
========================================================= */
- #earnXMR {
- font-size: 2rem;
- font-weight: 200;
- margin-bottom: 0.1rem;
- font-variant-numeric: tabular-nums;
+ .luck {
+ display: grid;
+ gap: 1rem;
+ grid-template-columns: 1fr;
}
- #earnEUR {
- font-size: 0.9rem;
- color: #6b7280;
- display: block;
- margin-top: 0.05rem;
- margin-bottom: 0.7rem;
- font-variant-numeric: tabular-nums;
+ /* A rule, not a nested card: the panel is already the container */
+ .luck__part + .luck__part {
+ padding-top: 1rem;
+ border-top: 1px solid var(--line);
}
- #eurThisWindow {
- font-size: 0.75rem;
- color: #6b7280;
- margin-top: 0.05rem;
- margin-bottom: 0.5rem;
- font-variant-numeric: tabular-nums;
+ .luck__label {
+ font-size: var(--step--1);
+ font-weight: 500;
+ color: var(--ink-3);
+ margin-bottom: 0.25rem;
}
- #totalEurEarned {
- font-size: 0.9rem;
- color: #6b7280;
- margin: 0;
- font-variant-numeric: tabular-nums;
+ /* =========================================================
+ SELECT
+ ========================================================= */
+ .select {
+ position: relative;
+ display: inline-flex;
}
- #paymentsCard .total-block {
- display: flex;
- flex-direction: column;
- gap: 0.1rem;
+ .select::after {
+ content: "";
+ position: absolute;
+ right: 0.6rem;
+ top: 50%;
+ width: 6px;
+ height: 6px;
+ border-right: 1.5px solid var(--ink-3);
+ border-bottom: 1.5px solid var(--ink-3);
+ transform: translateY(-70%) rotate(45deg);
+ pointer-events: none;
}
- .bottom-stats {
- margin-top: auto;
- display: flex;
- flex-direction: column;
- gap: 0.25rem;
- font-size: 0.8rem;
- color: #6b7280;
+ .select select {
+ appearance: none;
+ padding: 0.3rem 1.75rem 0.3rem 0.6rem;
+ background: var(--surface-raised);
+ color: var(--ink-2);
+ border: 1px solid var(--line);
+ border-radius: var(--radius-sm);
+ font: inherit;
+ font-size: var(--step--1);
+ cursor: pointer;
+ transition: border-color var(--speed) var(--ease);
+ }
+
+ .select select:hover {
+ border-color: var(--line-strong);
}
/* =========================================================
- PAYMENTS TABLE
+ TABLE
========================================================= */
+ .table-scroll {
+ overflow-x: auto;
+ }
+
#paymentsTable {
width: 100%;
- margin-top: 0.75rem;
border-collapse: collapse;
- }
-
- #paymentsTable thead tr {
- border-bottom: 1px solid #1f2937;
+ font-variant-numeric: tabular-nums;
}
#paymentsTable th {
- font-size: 0.65rem;
- text-transform: uppercase;
- letter-spacing: 1px;
- color: #4b5563;
- text-align: left;
- padding: 0.5rem 0;
+ position: sticky;
+ top: 0;
+ padding: 0.4rem 0.5rem;
+ background: var(--surface);
+ border-bottom: 1px solid var(--line);
+ font-size: var(--step--2);
font-weight: 500;
+ color: var(--ink-3);
+ text-align: left;
+ white-space: nowrap;
}
- #paymentsTable th:nth-child(2),
- #paymentsTable th:nth-child(3) {
- text-align: right;
+ #paymentsTable td {
+ padding: 0.45rem 0.5rem;
+ border-bottom: 1px solid var(--line);
+ font-family: var(--font-num);
+ font-size: var(--step--1);
+ color: var(--ink-2);
+ white-space: nowrap;
}
- #paymentsTable td {
- padding: 0.5rem 0;
- font-size: 0.8rem;
- color: #9ca3af;
- border-bottom: 1px solid #111827;
- font-variant-numeric: tabular-nums;
+ #paymentsTable tbody tr:last-child td {
+ border-bottom: 0;
}
- #paymentsTable td:nth-child(2),
- #paymentsTable td:nth-child(3) {
- text-align: right;
+ #paymentsTable tbody tr:hover td {
+ background: var(--surface-raised);
}
- /* Utility class for right-aligned table cells */
.cell-right {
text-align: right;
}
- #paymentsTable tbody tr:last-child td {
- border-bottom: none;
- }
-
/* =========================================================
- POOL STATUS & STATS
+ WORKERS
========================================================= */
- .stat-row {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
- gap: 1rem;
- margin-top: 0.75rem;
- }
-
- .stat-item {
+ .workers {
display: flex;
flex-direction: column;
- gap: 0.25rem;
- }
-
- .stat-label {
- font-size: 0.65rem;
- color: #4b5563;
- text-transform: uppercase;
- letter-spacing: 1px;
- font-weight: 500;
- }
-
- .stat-item .stat-value {
- font-size: 0.9rem;
- font-weight: 400;
- color: #e5e7eb;
- font-variant-numeric: tabular-nums;
- }
-
- /* =========================================================
- WORKERS LIST
- ========================================================= */
- .workers-list {
- margin-top: 0.75rem;
- max-height: 300px;
+ gap: 0.35rem;
+ max-height: 320px;
overflow-y: auto;
scrollbar-width: thin;
- scrollbar-color: #1f2937 transparent;
- }
-
- .workers-list::-webkit-scrollbar {
- width: 4px;
- }
-
- .workers-list::-webkit-scrollbar-track {
- background: transparent;
- }
-
- .workers-list::-webkit-scrollbar-thumb {
- background: #1f2937;
- border-radius: 2px;
+ scrollbar-color: var(--line-strong) transparent;
}
.worker-item {
- margin: 0.5rem 0;
- padding: 0.75rem;
- background: #111827;
- border-radius: 4px;
- font-size: 0.85rem;
display: flex;
- justify-content: space-between;
align-items: center;
- border: 1px solid #1f2937;
- transition: border-color 0.2s;
- }
-
- .worker-item:hover {
- border-color: #374151;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0.55rem 0.7rem;
+ background: var(--surface-raised);
+ border-radius: var(--radius-sm);
+ font-size: var(--step--1);
}
.worker-name {
- color: #e5e7eb;
- font-weight: 500;
+ color: var(--ink-2);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.worker-hash {
- color: #9ca3af;
+ font-family: var(--font-num);
+ color: var(--ink);
font-variant-numeric: tabular-nums;
+ white-space: nowrap;
}
.no-workers {
+ padding: 1.25rem 0;
text-align: center;
- opacity: 0.5;
- font-style: italic;
- padding: 1.5rem;
- color: #6b7280;
- font-size: 0.85rem;
+ font-size: var(--step--1);
+ color: var(--ink-3);
}
/* =========================================================
- MOBILE RESPONSIVENESS
+ LOADING SKELETON
+ Replaced by real text on first paint; never gates content
+ visibility, so a headless render still shows the markup.
========================================================= */
- @media (max-width: 1024px) {
- main.grid {
- grid-template-columns: repeat(6, 1fr);
- gap: 1rem;
- padding: 0 1rem 1.5rem;
- }
-
- #hashrateCard,
- #priceCard {
- grid-column: 1 / 7;
- grid-row: auto;
- }
-
- #paymentsCard,
- #combinedLuckCard,
- #earningsCard,
- #sharesCard {
- grid-column: 1 / 7;
- grid-row: auto;
- }
+ .skeleton {
+ display: inline-block;
+ width: 5ch;
+ height: 0.7em;
+ vertical-align: middle;
+ border-radius: 3px;
+ background: linear-gradient(
+ 90deg,
+ var(--surface-raised),
+ var(--line),
+ var(--surface-raised)
+ )
+ 0 0 / 200% 100%;
+ animation: sweep 1.4s linear infinite;
+ }
- #poolStatusCard,
- #workersCard {
- grid-column: 1 / 7;
- grid-row: auto;
+ @keyframes sweep {
+ to {
+ background-position: -200% 0;
}
}
- @media (max-width: 600px) {
- main.grid {
- grid-template-columns: 1fr;
- gap: 0.75rem;
- padding: 0 0.75rem 1rem;
+ /* =========================================================
+ BREAKPOINTS
+ Structural only: column counts and chart height, never
+ fluid type.
+ ========================================================= */
+ @media (min-width: 40rem) {
+ .luck {
+ grid-template-columns: 1fr 1fr;
}
- #hashrateCard,
- #priceCard,
- #paymentsCard,
- #earningsCard,
- #sharesCard,
- #combinedLuckCard,
- #poolStatusCard,
- #workersCard {
- grid-column: auto;
- grid-row: auto;
+ .luck__part + .luck__part {
+ padding-top: 0;
+ padding-left: 1rem;
+ border-top: 0;
+ border-left: 1px solid var(--line);
}
- .card {
- padding: 1rem;
+ .panels {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ }
- header h1 {
- font-size: 1.25rem;
+ @media (min-width: 64rem) {
+ :root {
+ --chart-h-tall: 340px;
}
- .card h2 {
- font-size: 0.65rem;
+ .trends__grid {
+ grid-template-columns: 1.7fr 1fr;
}
- .big {
- font-size: 1.5rem;
+ .panels {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
}
- .big-med {
- font-size: 1rem;
+ .panel--wide {
+ grid-column: span 2;
}
+ }
- .sub,
- .small-stat,
- .stat {
- font-size: 0.75rem;
+ @media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
}
- #earnXMR {
- font-size: 1.5rem;
- }
-
- #earnEUR {
- font-size: 0.8rem;
- }
-
- #combinedLuckCard .luck-grid {
- grid-template-columns: 1fr;
- }
-
- #combinedLuckCard .luck-section {
- padding: 0.5rem;
- }
-
- #earnPeriod {
- width: 100%;
- }
-
- .stat-row {
- grid-template-columns: 1fr 1fr;
+ .skeleton {
+ background: var(--surface-raised);
}
}
@@ data.dashboard.html @@
# v1/ConfigMap/web3/dashboard
! ± value change in multiline text (20 inserts, 20 deletions)
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
[two lines unchanged)]
<!-- Favicon for browser tab -->
<link
rel="icon"
type="image/png"
- href="data:image/png;base64,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"
+ href="data:image/png;base64,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"
/>
<!-- Responsive design for mobile and tablet -->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
[nine lines unchanged)]
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1"></script>
</head>
<body>
- <header>
- <!-- Dashboard title displayed at the top -->
- <h1>P2Pool Dashboard</h1>
- <div class="header-status">
- <span class="status-dot"></span>
- <span>Mining Performance Monitor</span>
+ <header class="masthead">
+ <div class="masthead__title">
+ <h1>P2Pool</h1>
+ <p class="masthead__sub">Solar-gated Monero mining</p>
+ </div>
+ <div class="masthead__state">
+ <span class="pill" id="minerState">
+ <span class="pill__dot"></span>
+ <span id="minerStateText">Connecting</span>
+ </span>
+ <span class="masthead__stamp" id="lastRefreshed"></span>
</div>
</header>
- <main class="grid">
- <!-- ==========================
- HASHRATE CARD (Top row)
- ========================== -->
- <div class="card" id="hashrateCard">
- <h2>Your Hashrate</h2>
- <div class="big" id="myHashrate">Loading…</div>
- <div class="sub">Pool share: <span id="poolShare">–</span></div>
- <div class="chart-container">
- <canvas id="hashrateChart"></canvas>
+ <main>
+ <!-- Headline figures, glanceable without scrolling -->
+ <section class="overview" aria-label="Current figures">
+ <div class="metric">
+ <span class="metric__label">Your hashrate</span>
+ <span class="metric__value" id="myHashrate"
+ ><span class="skeleton"></span
+ ></span>
+ <span class="metric__note">
+ <span id="poolShare">–</span> of pool
+ </span>
+ </div>
+ <div class="metric">
+ <span class="metric__label">XMR price</span>
+ <span class="metric__value" id="price"
+ ><span class="skeleton"></span
+ ></span>
+ <span class="metric__note">EUR, spot</span>
</div>
- </div>
-
- <!-- ==========================
- XMR PRICE CARD (Top row)
- ========================== -->
- <div class="card" id="priceCard">
- <h2>XMR Price</h2>
- <div class="big" id="price">Loading…</div>
- <div class="chart-container">
- <canvas id="priceChart"></canvas>
+ <div class="metric">
+ <span class="metric__label">Total earned</span>
+ <span class="metric__value" id="totalEarned"
+ ><span class="skeleton"></span
+ ></span>
+ <span class="metric__note" id="totalEurEarned">–</span>
</div>
- </div>
-
- <!-- ==========================
- RECENT PAYMENTS CARD (middle row)
- ========================== -->
- <div class="card" id="paymentsCard">
- <div class="total-block">
- <h2>Recent Payments</h2>
- <div class="sub">Total earned</div>
- <div class="big" id="totalEarned">Loading…</div>
- <div class="sub" id="totalEurEarned">–</div>
+ <div class="metric">
+ <span class="metric__label">Pool hashrate</span>
+ <span class="metric__value metric__value--sm" id="poolHashrate"
+ ><span class="skeleton"></span
+ ></span>
+ <span class="metric__note">
+ network <span id="netHashrate">–</span>
+ </span>
</div>
- <div id="paymentsStatus" class="small-stat">Loading…</div>
- <table id="paymentsTable">
- <thead>
- <tr>
- <th>Time</th>
- <th>Amount (XMR)</th>
- <th>Amount (€)</th>
- </tr>
- </thead>
- <tbody></tbody>
- </table>
- </div>
+ </section>
- <!-- ==========================
- EARNINGS CARD (middle row)
- ========================== -->
- <div class="card" id="earningsCard">
- <h2>Estimated Earnings</h2>
- <div class="big" id="earnXMR">
- Loading…
- <span class="tooltip-icon" id="earnTooltip">ⓘ</span>
- </div>
- <div class="sub" id="earnEUR">Loading…</div>
- <div class="small-stat" id="earnLegend">–</div>
- <div class="small-stat" id="lastRefreshed">–</div>
- <select id="earnPeriod">
- <option value="hour">per hour</option>
- <option value="day" selected>per day</option>
- <option value="week">per week</option>
- <option value="month">per month</option>
- <option value="year">per year</option>
- </select>
- <div class="bottom-stats">
- <div class="stat">
- <span>Payout Interval:</span>
- <span id="payoutInterval">Loading…</span>
- <span class="tooltip-icon">ⓘ</span>
- </div>
- <div class="small-stat">
- Network hashrate: <span id="netHashrate">–</span>
- </div>
- <div class="small-stat">
- Pool hashrate: <span id="poolHashrate">–</span>
- </div>
- <div class="small-stat">
- Block reward: <span id="blockReward">–</span> XMR
+ <!-- Trends: one range control drives both charts -->
+ <section class="trends" aria-labelledby="trendsHeading">
+ <div class="section-head">
+ <h2 id="trendsHeading">Trends</h2>
+ <div
+ class="range"
+ id="rangeControl"
+ role="group"
+ aria-label="Time range"
+ >
+ <button type="button" class="range__opt" data-hours="1">1h</button>
+ <button type="button" class="range__opt" data-hours="6">6h</button>
+ <button
+ type="button"
+ class="range__opt"
+ data-hours="24"
+ aria-pressed="true"
+ >
+ 24h
+ </button>
+ <button type="button" class="range__opt" data-hours="168">
+ 7d
+ </button>
+ <button type="button" class="range__opt" data-hours="720">
+ 30d
+ </button>
+ <button type="button" class="range__opt" data-hours="2160">
+ 90d
+ </button>
</div>
</div>
- </div>
- <!-- ==========================
- SHARES & UNCLES CARD (middle row)
- ========================== -->
- <div class="card" id="sharesCard">
- <h2>Shares & Uncles</h2>
- <div class="big">Shares: <span id="sharesSinceLastPayout">–</span></div>
- <div class="sub-sub">Shares since the last payout</div>
- <div class="big">Uncles: <span id="unclesSinceLastPayout">–</span></div>
- <div class="sub-sub">Uncles since the last payout</div>
- <div class="bottom-stats">
- <div class="small-stat" id="totalSharesMined">–</div>
- <div class="small-stat" id="totalUnclesMined">–</div>
- </div>
- </div>
-
- <!-- ==========================
- COMBINED LUCK CARD
- ========================== -->
- <div class="card" id="combinedLuckCard">
- <h2>Luck Overview</h2>
- <div class="luck-grid">
- <!-- PPLNS Window Luck -->
- <div class="luck-section" id="pplnsLuck">
- <h3>PPLNS Window Luck</h3>
- <div class="big">
- <span id="luckFactor">–</span>⨯
- <span class="tooltip-icon" id="luckTooltip">ⓘ</span>
- </div>
- <div class="big-med">Accumulated XMR:</div>
- <div class="sub"><span id="xmrThisWindow">0.000000</span> XMR</div>
- <div class="sub" id="eurThisWindow">–</div>
- <div class="sub-sub" id="luckInfo">
- Only true if pool mines expected blocks based on net share this
- window and avg pool effort; minimum payout 0.00027 XMR
- </div>
- <div class="bottom-stats">
- <div class="small-stat">
- Start of PPLNS window: <span id="pplnsStart">Loading...</span>
- </div>
- <div class="small-stat">
- PPLNS Window Hashrate: <span id="dayHash">–</span>
- </div>
- <div class="small-stat">
- Current pool effort: <span id="currentEffort">–</span>%
- </div>
+ <div class="trends__grid">
+ <article class="panel">
+ <h3 class="panel__title">Hashrate</h3>
+ <div class="chart chart--tall">
+ <canvas id="hashrateChart"></canvas>
+ <p class="chart__empty" id="hashrateEmpty" hidden>
+ No samples in this range yet. Longer ranges fill in as the
+ logger runs.
+ </p>
</div>
- </div>
+ </article>
- <!-- Estimated True Luck -->
- <div class="luck-section" id="trueLuck">
- <h3>Estimated True Luck</h3>
- <div class="big">
- <span id="trueLuckFactor">–</span>⨯
- <span class="tooltip-icon" id="trueLuckTooltip">ⓘ</span>
- </div>
- <div class="sub" id="trueLuckWindow">–</div>
- <div class="sub-sub" id="trueLuckInfo">
- Longer time windows reduce accuracy; uses a 24h moving average of
- hashrates.
+ <article class="panel">
+ <h3 class="panel__title">XMR price</h3>
+ <div class="chart chart--tall">
+ <canvas id="priceChart"></canvas>
+ <p class="chart__empty" id="priceEmpty" hidden>
+ No samples in this range yet.
+ </p>
</div>
- </div>
+ </article>
</div>
- </div>
+ </section>
- <!-- ==========================
- POOL STATUS & STATS CARD (from old dashboard)
- ========================== -->
- <div class="card" id="poolStatusCard">
- <h2>Pool Status & Statistics</h2>
- <div class="stat-row">
- <div class="stat-item">
- <div class="stat-label">Pool Status</div>
- <div class="stat-value">
- <span class="status-indicator" id="pool-status"></span>
- <span id="pool-status-text">Unknown</span>
+ <section class="panels" aria-label="Mining detail">
+ <!-- Estimated earnings -->
+ <article class="panel" id="earningsCard">
+ <div class="panel__head">
+ <h3 class="panel__title">Estimated earnings</h3>
+ <label class="select">
+ <span class="visually-hidden">Earnings period</span>
+ <select id="earnPeriod">
+ <option value="hour">per hour</option>
+ <option value="day" selected>per day</option>
+ <option value="week">per week</option>
+ <option value="month">per month</option>
+ <option value="year">per year</option>
+ </select>
+ </label>
+ </div>
+ <p class="figure">
+ <span id="earnXMR"><span class="skeleton"></span></span>
+ <span class="info" id="earnTooltip" tabindex="0" role="note"
+ >i</span
+ >
+ </p>
+ <p class="figure__note" id="earnEUR">–</p>
+ <p class="footnote" id="earnLegend">–</p>
+ <dl class="facts bottom-stats">
+ <div class="fact">
+ <dt>Payout interval <span class="info">i</span></dt>
+ <dd id="payoutInterval">–</dd>
+ </div>
+ <div class="fact">
+ <dt>Block reward</dt>
+ <dd><span id="blockReward">–</span> XMR</dd>
</div>
+ </dl>
+ </article>
+
+ <!-- Luck -->
+ <article class="panel panel--wide" id="combinedLuckCard">
+ <h3 class="panel__title">Luck</h3>
+ <div class="luck">
+ <section class="luck__part">
+ <h4 class="luck__label">
+ PPLNS window
+ <span class="info" id="luckTooltip" tabindex="0" role="note"
+ >i</span
+ >
+ </h4>
+ <p class="figure figure--sm">
+ <span id="luckFactor">–</span>⁠⨯
+ </p>
+ <p class="figure__note">
+ <span id="xmrThisWindow">0.000000</span> XMR accumulated
+ </p>
+ <p class="figure__note" id="eurThisWindow">–</p>
+ <p class="footnote" id="luckInfo">
+ Only true if the pool mines its expected blocks for this
+ window's network share and average effort; minimum payout
+ 0.00027 XMR.
+ </p>
+ <dl class="facts">
+ <div class="fact">
+ <dt>Window start</dt>
+ <dd id="pplnsStart">–</dd>
+ </div>
+ <div class="fact">
+ <dt>Window hashrate</dt>
+ <dd id="dayHash">–</dd>
+ </div>
+ <div class="fact">
+ <dt>Pool effort</dt>
+ <dd><span id="currentEffort">–</span>%</dd>
+ </div>
+ </dl>
+ </section>
+
+ <section class="luck__part">
+ <h4 class="luck__label">
+ Estimated true
+ <span class="info" id="trueLuckTooltip" tabindex="0" role="note"
+ >i</span
+ >
+ </h4>
+ <p class="figure figure--sm">
+ <span id="trueLuckFactor">–</span>⁠⨯
+ </p>
+ <p class="figure__note" id="trueLuckWindow">–</p>
+ <p class="footnote" id="trueLuckInfo">
+ Longer windows reduce accuracy; uses a 24h moving average of
+ hashrates.
+ </p>
+ </section>
</div>
- <div class="stat-item">
- <div class="stat-label">Your Hashrate (24h)</div>
- <div class="stat-value" id="user-hashrate-24h">–</div>
+ </article>
+
+ <!-- Shares -->
+ <article class="panel" id="sharesCard">
+ <h3 class="panel__title">Shares & uncles</h3>
+ <div class="pair">
+ <p class="figure figure--sm" id="sharesSinceLastPayout">–</p>
+ <p class="figure__note">Shares since last payout</p>
</div>
- <div class="stat-item">
- <div class="stat-label">Shares Found (Total)</div>
- <div class="stat-value" id="shares-found">–</div>
+ <div class="pair">
+ <p class="figure figure--sm" id="unclesSinceLastPayout">–</p>
+ <p class="figure__note">Uncles since last payout</p>
</div>
- <div class="stat-item">
- <div class="stat-label">Shares Failed (Total)</div>
- <div class="stat-value" id="shares-failed">–</div>
- </div>
- <div class="stat-item">
- <div class="stat-label">Active Connections</div>
- <div class="stat-value" id="connections">–</div>
- </div>
- <div class="stat-item">
- <div class="stat-label">Block Reward Share</div>
- <divThis message was truncated. Download full message |
|
Warning Review limit reached
Next review available in: 56 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe Monero dashboard now uses a responsive dark theme, structured panels, selectable chart ranges, persisted chart history, and a new server endpoint with tiered retention and downsampling. Nginx proxies the history endpoint. Server tests cover rollups and filtering. Prometheus alerts use a shared node-to-Deployment recording rule. ChangesMonero dashboard history and redesign
Monero guard alert mapping
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Operator as Dashboard operator
participant Browser as dashboard.js
participant Nginx as nginx
participant Server as server.py
Operator->>Browser: Select chart range
Browser->>Nginx: Request stats_history.json
Nginx->>Server: Proxy chart-history request
Server-->>Nginx: Return downsampled history
Nginx-->>Browser: Return chart data
Browser-->>Operator: Render charts and status
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
AI Automated Review (incremental)Analysis engine: omniroute@http://litellm.ai.svc.cluster.local/v1 (openai) Recommendation: Approve. The incremental fixes address input validation and alert consistency without introducing regressions. Change-by-change findings
Standards Compliance
Tool Harness Findings
Unknowns or Needs Verification
Unaddressed required checksThe classifier marked these checks as required for this PR's risk profile, but the review above does not appear to discuss them:
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (1)
kubernetes/apps/web3/monero/dashboard/resources/test_server.py (1)
49-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider covering the
hoursclamping inserve_history.The tests cover
accumulate_rollupanddownsample. They do not cover the tier-selection and clamping logic inserve_history, which is the contract the dashboard depends on. A small unit test on the parsing branch would catch a regression in theMAX_LOG_AGEboundary or in the default value.Do you want me to draft that test?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@kubernetes/apps/web3/monero/dashboard/resources/test_server.py` around lines 49 - 82, Extend DownsampleTest with a focused unit test for serve_history’s hours parsing branch, covering both the default hours value and clamping requested hours to MAX_LOG_AGE. Assert the selected history window or resulting response reflects the clamped boundary, while preserving existing behavior for valid in-range input.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@kubernetes/apps/web3/monero/dashboard/resources/dashboard.css`:
- Around line 32-37: Quote the named font families flagged by value-keyword-case
in the --font-ui and --font-num declarations, including Roboto, Helvetica,
Arial, Menlo, and Consolas, while preserving the existing fallback order and
font stacks.
In `@kubernetes/apps/web3/monero/dashboard/resources/dashboard.html`:
- Around line 152-157: Update the earnings markup and its text-update target so
`#earnTooltip` is outside `#earnXMR` and remains available after refreshes. Add or
use a dedicated value span inside `#earnXMR` for setTextContent, and change the
corresponding update logic to target that span while preserving
updateEarningsTooltip() access to `#earnTooltip`.
In `@kubernetes/apps/web3/monero/dashboard/resources/dashboard.js`:
- Around line 644-663: Update fetchChartWindow and the fetch path in
fetchDashboardData to capture the selected range alongside each request, then
have applyChartWindow accept that range and ignore responses whose range no
longer matches state.currentRangeHours. Rename applyChartWindow’s window
parameter to avoid shadowing the global window object, while preserving
valid-response handling for the currently selected range.
In `@kubernetes/apps/web3/monero/dashboard/resources/server.py`:
- Around line 270-287: Update load_log_disk’s rollup seeding loop to replay only
up to the shortest column length across timestamps and all SERIES values,
preventing indexing beyond ragged data while preserving normal seeding behavior.
---
Nitpick comments:
In `@kubernetes/apps/web3/monero/dashboard/resources/test_server.py`:
- Around line 49-82: Extend DownsampleTest with a focused unit test for
serve_history’s hours parsing branch, covering both the default hours value and
clamping requested hours to MAX_LOG_AGE. Assert the selected history window or
resulting response reflects the clamped boundary, while preserving existing
behavior for valid in-range input.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9bedc3d1-bc80-4de9-84b5-b731db5a2c6c
📒 Files selected for processing (6)
kubernetes/apps/web3/monero/dashboard/resources/dashboard.csskubernetes/apps/web3/monero/dashboard/resources/dashboard.htmlkubernetes/apps/web3/monero/dashboard/resources/dashboard.jskubernetes/apps/web3/monero/dashboard/resources/nginx.confkubernetes/apps/web3/monero/dashboard/resources/server.pykubernetes/apps/web3/monero/dashboard/resources/test_server.py
Merge 3 per-file configMap subPath mounts into one directory mount, enable sendfile (no reason it was off under readOnlyRootFilesystem), and inline three single-call wrapper functions in dashboard.js.
@@ spec.template.spec.containers.web.volumeMounts @@
# apps/v1/Deployment/web3/dashboard
! - three list entries removed:
- - name: dashboard-html
- mountPath: /usr/share/nginx/html/index.html
- subPath: dashboard.html
- - name: dashboard-html
- mountPath: /usr/share/nginx/html/dashboard.css
- subPath: dashboard.css
- - name: dashboard-html
- mountPath: /usr/share/nginx/html/dashboard.js
- subPath: dashboard.js
! + one list entry added:
+ - name: dashboard-html
+ mountPath: /usr/share/nginx/html
|
…nx routes log_loop fired xmrig and monerod HTTP calls sequentially (5s timeout each, worst case stacked); fetch them concurrently via a small ThreadPoolExecutor instead. nginx also carried its own hand-synced copy of server.py's do_GET route list. Replace it with a single fallback location: unmatched static paths proxy to the Python backend, so nginx no longer needs to know the route set at all.
- Quote value-keyword font-family names in dashboard.css (stylelint
value-keyword-case)
- Move #earnTooltip out of #earnXMR so setTextContent("earnXMR", ...)
no longer wipes the tooltip badge on the first refresh
- Stamp chart-window fetches with the requested range and drop stale
responses, so a range switch mid-poll can't get overwritten by an
in-flight response for the old range
- Guard rollup seeding against a ragged stats_log.json column instead
of indexing past its length and crashing startup
…g rule XmrigGuardEnforcementBypassed and XmrigGuardThermalPanic each ran their own label_replace encoding the "xmrig-<node>" Deployment naming convention; a rename required editing both in lockstep. Move it into a single xmrig_guard_deployment_node recording rule both alerts join through instead.
|
@coderabbitai review |
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
kubernetes/apps/web3/monero/dashboard/resources/server.py (1)
236-242: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winReject non-finite
hoursvalues.
float("nan")succeeds at Line 239. The clamp at Line 242 keepsNaN. Line 245 then selectsrollup, anddownsamplereceives an invalid window instead of the default window.Proposed fix
try: hours = float(parse_qs(query).get("hours", ["24"])[0]) except ValueError: hours = 24.0 + if not math.isfinite(hours): + hours = 24.0 hours = min(max(hours, 0.1), ROLLUP_MAX_AGE / 3600)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@kubernetes/apps/web3/monero/dashboard/resources/server.py` around lines 236 - 242, Update serve_history to reject non-finite parsed hours values, including NaN and infinities, before applying the min/max clamp. Fall back to the default 24.0-hour window for any non-finite value while preserving the existing handling and bounds for valid finite inputs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@kubernetes/apps/web3/monero/dashboard/resources/server.py`:
- Around line 236-242: Update serve_history to reject non-finite parsed hours
values, including NaN and infinities, before applying the min/max clamp. Fall
back to the default 24.0-hour window for any non-finite value while preserving
the existing handling and bounds for valid finite inputs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 39df189f-34b3-4a6a-9d19-9a5c63bec1a4
📒 Files selected for processing (7)
kubernetes/apps/web3/monero/dashboard/helmrelease.yamlkubernetes/apps/web3/monero/dashboard/resources/dashboard.csskubernetes/apps/web3/monero/dashboard/resources/dashboard.htmlkubernetes/apps/web3/monero/dashboard/resources/dashboard.jskubernetes/apps/web3/monero/dashboard/resources/nginx.confkubernetes/apps/web3/monero/dashboard/resources/server.pykubernetes/apps/web3/monero/guard/prometheusrule.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
- kubernetes/apps/web3/monero/dashboard/resources/dashboard.html
- kubernetes/apps/web3/monero/dashboard/resources/dashboard.css
NaN/Infinity survives float() and the min/max clamp, sending downsample() an invalid window instead of the 24h default.
|
@coderabbitai review |
|
…iers xmrig reports hashrate.total[0] as null while the solar gate holds the miner idle. That null was appended straight into the log, where it broke accumulate_rollup's += and downsample's sum(). The 24h file on the PVC already held 14 of them, so the rollup back-fill added in #4394 crashed on every start — dashboard-server had 299 restarts and every endpoint it owns returned 502. Coerce to a finite float at the two points values enter the tiers (append_log for live samples, load_tier for files written before this), so idle reads as 0 H/s rather than poisoning both retention tiers. The existing ragged-column guard checked lengths, not values.
The logger kept one tier — 10s samples, 24h, then evicted — so no range longer than a day had data behind it.
Retention
/stats_history.json?hours=Nmean-aggregates whichever tier covers the range to a fixed 720-point budget. Mean not decimation: a miner gated off for part of a bucket pulls that bucket down instead of vanishing between samples. On first start the rollup back-fills from the 24h log, so 7d is populated immediately rather than blank for three months.Chart ranges: 1h/6h/24h/7d/30d/90d, persisted in localStorage.
/stats_log.jsonand the earnings/luck maths are untouched.Interface
#4b5563on#0a0a0a≈ 3:1, under the 4.5:1 floor--surfaceLoading…strings, no focus rings, no reduced-motion path:focus-visible,prefers-reduced-motionTwo display bugs fixed in passing: accumulated XMR printed 12 decimals (one atomic unit), and the all-time counters repeated their own labels.
Verification
test_server.py, 5 tests: bucket close, retention eviction, point cap, window trim, empty source. Caught a real error during development.node --check,oxfmt,kustomize buildpass; nginx route regex covers the new endpoint.Not verified: no interaction testing (headless Firefox, no extension) — range buttons and the period select are verified by reading. Nothing has run against the live pod.
Known limitation
At 90d the hashrate reads as a picket fence: 90 daily solar cycles land in 720 buckets. Honest but dense. A daily-average overlay would be the follow-up if the trend matters more than the cycles.
Summary by CodeRabbit
New Features
Bug Fixes