From 8aab930b254123ba2ba03c5c60010ecb16453e6a Mon Sep 17 00:00:00 2001 From: Andy Anderson Date: Mon, 5 Oct 2026 11:36:51 -0400 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9C=A8=20dashboard:=20add=20review=20que?= =?UTF-8?q?ue=20history=20sparkline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andy Anderson --- ...-review-queue-collapsed-sparkline-10610.md | 1 + src/pkg/dashboard/api_review_queue.go | 67 ++++++++-- src/pkg/dashboard/api_review_queue_test.go | 15 +++ .../dashboard/review_queue_view_9607_test.go | 19 +++ src/pkg/dashboard/static/index.html | 114 +++++++++++++++++- 5 files changed, 208 insertions(+), 8 deletions(-) create mode 100644 src/changelog.d/changed-review-queue-collapsed-sparkline-10610.md diff --git a/src/changelog.d/changed-review-queue-collapsed-sparkline-10610.md b/src/changelog.d/changed-review-queue-collapsed-sparkline-10610.md new file mode 100644 index 0000000000..e46296ee92 --- /dev/null +++ b/src/changelog.d/changed-review-queue-collapsed-sparkline-10610.md @@ -0,0 +1 @@ +- Changed the Review Queue collapsed row to show priority-split history with shared sparkline range controls (#10610) diff --git a/src/pkg/dashboard/api_review_queue.go b/src/pkg/dashboard/api_review_queue.go index d96f347b23..b1cd384e29 100644 --- a/src/pkg/dashboard/api_review_queue.go +++ b/src/pkg/dashboard/api_review_queue.go @@ -6,6 +6,7 @@ import ( "os" "strconv" "strings" + "sync" "time" ghpkg "github.com/hivecommons/hive/pkg/github" @@ -20,15 +21,32 @@ const ( reviewQueueMaxLimit = 200 ) +type reviewQueueHistoryStore struct { + mu sync.Mutex + entries []ReviewQueueHistoryEntry +} + +var reviewQueueHistoryByServer sync.Map + +// ReviewQueueHistoryEntry is one priority-split review queue depth sample. +type ReviewQueueHistoryEntry struct { + Timestamp int64 `json:"t"` + Total int `json:"total"` + High int `json:"high"` + Normal int `json:"normal"` + Low int `json:"low"` +} + // reviewQueueResponse is one page of the PR review queue. type reviewQueueResponse struct { - GeneratedAt time.Time `json:"generated_at"` - SnapshotAt time.Time `json:"snapshot_at,omitzero"` - Total int `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` - HasMore bool `json:"has_more"` - Items []ghpkg.ReviewQueueEntry `json:"items"` + GeneratedAt time.Time `json:"generated_at"` + SnapshotAt time.Time `json:"snapshot_at,omitzero"` + Total int `json:"total"` + Limit int `json:"limit"` + Offset int `json:"offset"` + HasMore bool `json:"has_more"` + History []ReviewQueueHistoryEntry `json:"history"` + Items []ghpkg.ReviewQueueEntry `json:"items"` } // handleReviewQueue serves GET /api/review/queue?limit=N&offset=M: every open @@ -65,6 +83,7 @@ func (s *Server) handleReviewQueue(w http.ResponseWriter, r *http.Request) { } actionable := s.lastActionableForPRModels() queue := ghpkg.ReviewQueueFromActionable(actionable, opts) + history := s.recordReviewQueueHistory(queue, opts.Now) resp := reviewQueueResponse{ GeneratedAt: opts.Now.UTC(), @@ -72,6 +91,7 @@ func (s *Server) handleReviewQueue(w http.ResponseWriter, r *http.Request) { Total: len(queue), Limit: limit, Offset: offset, + History: history, Items: []ghpkg.ReviewQueueEntry{}, } if offset < len(queue) { @@ -109,3 +129,36 @@ func reviewQueuePaging(r *http.Request) (limit, offset int, err error) { } return limit, offset, nil } + +func (s *Server) recordReviewQueueHistory(queue []ghpkg.ReviewQueueEntry, now time.Time) []ReviewQueueHistoryEntry { + if s == nil { + return nil + } + entry := ReviewQueueHistoryEntry{Timestamp: now.UTC().UnixMilli(), Total: len(queue)} + for _, item := range queue { + switch item.Priority { + case ghpkg.ReviewPriorityHigh: + entry.High++ + case ghpkg.ReviewPriorityLow: + entry.Low++ + default: + entry.Normal++ + } + } + + storeAny, _ := reviewQueueHistoryByServer.LoadOrStore(s, &reviewQueueHistoryStore{}) + store := storeAny.(*reviewQueueHistoryStore) + store.mu.Lock() + defer store.mu.Unlock() + if n := len(store.entries); n > 0 && entry.Timestamp-store.entries[n-1].Timestamp < trendHistoryMinIntervalMs { + store.entries[n-1] = entry + } else { + store.entries = append(store.entries, entry) + } + if len(store.entries) > trendHistoryMaxEntries { + store.entries = store.entries[len(store.entries)-trendHistoryMaxEntries:] + } + out := make([]ReviewQueueHistoryEntry, len(store.entries)) + copy(out, store.entries) + return out +} diff --git a/src/pkg/dashboard/api_review_queue_test.go b/src/pkg/dashboard/api_review_queue_test.go index c5738f7f45..a94a246be7 100644 --- a/src/pkg/dashboard/api_review_queue_test.go +++ b/src/pkg/dashboard/api_review_queue_test.go @@ -94,6 +94,21 @@ func TestHandleReviewQueue_RanksAndPages(t *testing.T) { } } +func TestHandleReviewQueue_IncludesPriorityHistory(t *testing.T) { + s := reviewQueueTestServer(t, filepath.Join(t.TempDir(), "missing.json")) + resp := decodeReviewQueue(t, doOwnerGet(s, "/api/review/queue")) + if len(resp.History) != 1 { + t.Fatalf("history len = %d, want 1", len(resp.History)) + } + entry := resp.History[0] + if entry.Total != 3 || entry.High != 1 || entry.Normal != 1 || entry.Low != 1 { + t.Fatalf("history priority split = %+v, want total/high/normal/low 3/1/1/1", entry) + } + if entry.Timestamp <= 0 { + t.Fatalf("history timestamp = %d, want unix millis", entry.Timestamp) + } +} + func TestHandleReviewQueue_RejectsBadPaging(t *testing.T) { s := reviewQueueTestServer(t, filepath.Join(t.TempDir(), "missing.json")) for _, q := range []string{"limit=0", "limit=-1", "limit=abc", "limit=201", "offset=-1", "offset=x"} { diff --git a/src/pkg/dashboard/review_queue_view_9607_test.go b/src/pkg/dashboard/review_queue_view_9607_test.go index 772b93b632..b9fafdc261 100644 --- a/src/pkg/dashboard/review_queue_view_9607_test.go +++ b/src/pkg/dashboard/review_queue_view_9607_test.go @@ -26,6 +26,25 @@ func TestReviewQueueViewStaticWiring(t *testing.T) { t.Errorf("index.html is missing review-queue wiring %q", want) } } + // The collapsed Review Queue card must use the shared range-control and + // renderSparkline path, not the old count-only gauge. + for _, want := range []string{ + `function reviewQueueCollapsedSummary(text)`, + `function reviewQueueRangeControls()`, + `overview-kpi-range-btn`, + `data-action="setReviewQueueWindow"`, + `function reviewQueueSparkline(entries)`, + `renderSparkline(null, values`, + `['high', high, 'High priority']`, + } { + if !strings.Contains(html, want) { + t.Errorf("index.html is missing review-queue collapsed sparkline wiring %q", want) + } + } + if strings.Contains(html, `sectionId === 'review-queue-section') { + inner = miniGauge`) { + t.Error("Review Queue collapsed summary still uses the gauge renderer") + } // The view must never fall back to a browser-native dialog. for _, banned := range []string{"window.alert", "window.confirm", "window.prompt"} { if strings.Contains(jsFunc(t, html, "renderReviewQueue"), banned) { diff --git a/src/pkg/dashboard/static/index.html b/src/pkg/dashboard/static/index.html index 1130a0d25f..b0963ee24f 100644 --- a/src/pkg/dashboard/static/index.html +++ b/src/pkg/dashboard/static/index.html @@ -3813,6 +3813,24 @@ .review-queue-reasons { margin: var(--sp-2) 0 0; padding-left: var(--sp-6); color: var(--muted); font-size: var(--fs-xs); } .review-queue-noreasons { color: var(--muted); font-size: var(--fs-xs); margin-top: var(--sp-2); } .review-queue-empty { color: var(--terminal-muted); font-size: var(--fs-base); padding: var(--sp-4); } + .review-queue-collapsed { width: min(460px, 44vw); max-width: 460px; gap: var(--sp-3); } + .review-queue-collapsed-main { display: inline-flex; align-items: center; gap: var(--sp-2); min-width: 0; } + .review-queue-collapsed-total { color: var(--text); font-weight: 900; font-variant-numeric: tabular-nums; } + .review-queue-chip { display: inline-flex; align-items: center; gap: var(--sp-1); padding: var(--sp-0) var(--sp-2); border: 1px solid var(--blue); border-radius: var(--r-pill); color: var(--muted); font-size: var(--fs-2xs); font-weight: 800; font-variant-numeric: tabular-nums; } + .review-queue-chip.high { border-color: var(--red); } + .review-queue-chip.low { border-color: var(--muted); } + .review-queue-delta { font-size: var(--fs-2xs); font-weight: 900; font-variant-numeric: tabular-nums; } + .review-queue-delta.up { color: var(--red); } + .review-queue-delta.down { color: var(--green); } + .review-queue-delta.flat { color: var(--muted); } + .review-queue-spark { flex: 1 1 120px; min-width: 96px; } + .review-queue-spark .sparkline { width: 100%; } + .review-queue-spark svg { width: 100%; height: 24px; } + .review-queue-priority-line { fill: none; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; opacity: 0.9; } + .review-queue-priority-line.high { stroke: var(--red); } + .review-queue-priority-line.normal { stroke: var(--blue); } + .review-queue-priority-line.low { stroke: var(--muted); } + .review-queue-collapsed-range { position: static; flex: 0 0 auto; } .review-priority { display: inline-block; padding: var(--sp-1) var(--sp-4); border-radius: var(--r-sm); font-size: var(--fs-2xs); color: var(--muted); border: 1px solid var(--blue); } .review-priority-high { border-color: var(--red); } .review-priority-low { border-color: var(--muted); } @@ -5401,6 +5419,20 @@

What sh } const MINI_SUMMARY_HISTORY_PREFIX = 'hive_visual_summary_history:'; const MINI_SUMMARY_HISTORY_POINTS = 48; + const REVIEW_QUEUE_WINDOW_KEY = 'hive_review_queue_history_window'; + const REVIEW_QUEUE_MS_PER_HOUR = 3600e3; + const REVIEW_QUEUE_HOURS_PER_DAY = 24; + const REVIEW_QUEUE_DAYS_PER_WEEK = 7; + const REVIEW_QUEUE_WINDOWS = { '24h': REVIEW_QUEUE_HOURS_PER_DAY * REVIEW_QUEUE_MS_PER_HOUR, '7d': REVIEW_QUEUE_DAYS_PER_WEEK * REVIEW_QUEUE_HOURS_PER_DAY * REVIEW_QUEUE_MS_PER_HOUR }; + const REVIEW_QUEUE_SPARK_W = 128; + const REVIEW_QUEUE_SPARK_H = 24; + const REVIEW_QUEUE_SPARK_PAD_Y = 2; + let _reviewQueueWindow = (() => { + try { + const saved = localStorage.getItem(REVIEW_QUEUE_WINDOW_KEY); + return Object.prototype.hasOwnProperty.call(REVIEW_QUEUE_WINDOWS, saved) ? saved : '24h'; + } catch { return '24h'; } + })(); function miniHiveKey() { return String(((window._lastStatus || {}).hiveId) || 'default').trim() || 'default'; } @@ -5495,6 +5527,84 @@

What sh const title = label || text; return `${miniTitleTag(title)}${escapeHtml(text.slice(0, 10))}`; } + function reviewQueueHistoryEntries() { + const cutoff = Date.now() - (REVIEW_QUEUE_WINDOWS[_reviewQueueWindow] || REVIEW_QUEUE_WINDOWS['24h']); + const rows = Array.isArray(window._reviewQueueHistory) ? window._reviewQueueHistory : []; + return rows.filter(row => { + const t = Number(row && row.t); + return Number.isFinite(t) && t >= cutoff; + }); + } + function reviewQueueSeries(entries, key) { + return (entries || []).map(row => Number(row && row[key]) || 0); + } + function reviewQueueSparkPoints(values, min, max) { + const series = (Array.isArray(values) ? values : []).map(v => Number(v) || 0); + const range = (max - min) || max || 1; + const plotH = REVIEW_QUEUE_SPARK_H - REVIEW_QUEUE_SPARK_PAD_Y * 2; + if (series.length <= 1) { + const y = REVIEW_QUEUE_SPARK_PAD_Y + plotH - (((series[0] || 0) - min) / range) * plotH; + return `0,${y.toFixed(1)} ${REVIEW_QUEUE_SPARK_W.toFixed(1)},${y.toFixed(1)}`; + } + return series.map((v, i) => { + const x = (i / (series.length - 1)) * REVIEW_QUEUE_SPARK_W; + const y = REVIEW_QUEUE_SPARK_PAD_Y + plotH - ((v - min) / range) * plotH; + return `${x.toFixed(1)},${y.toFixed(1)}`; + }).join(' '); + } + function reviewQueueRangeControls() { + return `${Object.keys(REVIEW_QUEUE_WINDOWS).map(win => ``).join('')}`; + } + function reviewQueueSparkline(entries) { + const total = reviewQueueSeries(entries, 'total'); + const high = reviewQueueSeries(entries, 'high'); + const normal = reviewQueueSeries(entries, 'normal'); + const low = reviewQueueSeries(entries, 'low'); + const values = total.length ? total : [0, 0]; + const max = Math.max(1, ...values, ...high, ...normal, ...low); + const latest = values[values.length - 1] || 0; + const min = Math.min(0, ...values); + const title = `min ${fmtSparkVal(min)} · max ${fmtSparkVal(max)} · latest ${fmtSparkVal(latest)}`; + const after = [ + ['high', high, 'High priority'], + ['normal', normal, 'Normal priority'], + ['low', low, 'Low priority'], + ].map(([key, series, label]) => series.length ? `${escapeHtml(label)}: latest ${fmtSparkVal(series[series.length - 1] || 0)}` : '').join(''); + return `${renderSparkline(null, values, { + width: REVIEW_QUEUE_SPARK_W, height: REVIEW_QUEUE_SPARK_H, min, max, color: 'color-mix(in srgb, var(--purple) 60%, transparent)', + title, ariaLabel: `Review Queue total: ${title}`, label: 'review queue total', id: 'review-queue-collapsed', + dot: false, strokeWidth: 1, after, wrapperClass: 'sparkline--flush', wrapperTitle: title, + })}`; + } + function reviewQueueCollapsedSummary(text) { + const entries = reviewQueueHistoryEntries(); + const latest = entries[entries.length - 1] || {}; + const total = Number(latest.total); + const high = Number(latest.high) || 0; + const normal = Number(latest.normal) || 0; + const low = Number(latest.low) || 0; + const shownTotal = Number.isFinite(total) ? total : miniFirstNumber(text); + const first = entries[0] || {}; + const startTotal = Number(first.total); + const delta = Number.isFinite(startTotal) ? shownTotal - startTotal : 0; + const deltaCls = delta > 0 ? 'up' : delta < 0 ? 'down' : 'flat'; + const deltaText = delta > 0 ? `▲${delta}` : delta < 0 ? `▼${Math.abs(delta)}` : '•0'; + const chips = [ + ['high', 'H', high], + ['normal', 'N', normal], + ['low', 'L', low], + ].map(([key, label, value]) => `${esc(label)} ${fmtSparkVal(value)}`).join(''); + const label = `${fmtSparkVal(shownTotal)} PRs · high ${high}, normal ${normal}, low ${low}, ${deltaText} vs range start`; + const inner = `${reviewQueueRangeControls()}${fmtSparkVal(shownTotal)}${chips}${deltaText}${reviewQueueSparkline(entries)}`; + return `${inner}`; + } + function setReviewQueueWindow(win) { + if (!Object.prototype.hasOwnProperty.call(REVIEW_QUEUE_WINDOWS, win)) return; + _reviewQueueWindow = win; + try { localStorage.setItem(REVIEW_QUEUE_WINDOW_KEY, win); } catch {} + setSectionSummary('review-queue-section', _reviewQueueTotal ? _reviewQueueTotal + ' PR' + (_reviewQueueTotal === 1 ? '' : 's') : '0 PRs'); + } + window.setReviewQueueWindow = setReviewQueueWindow; function miniAvatarDots(contributors, total, label) { const list = Array.isArray(contributors) ? contributors.slice(0, 5) : []; const dots = list.map((c, i) => `${escapeHtml((c && (c.github_username || c.login || c.name)) || 'contributor')}`).join(''); @@ -5545,7 +5655,7 @@

What sh } else if (sectionId === 'acmm-eval-section' || sectionId === 'acmm-reco-section') { inner = miniDial(n0, ['L0','L1','L2','L3','L4','L5','L6'], text); } else if (sectionId === 'review-queue-section') { - inner = miniGauge(n0, Math.max(10, n0), 'var(--purple)', text); + return reviewQueueCollapsedSummary(text); } else if (sectionId === 'inception-section') { inner = miniBar([{ label: 'proposals', value: n0, color: 'var(--blue)' }, { label: 'in progress', value: nums[1] || 0, color: 'var(--green)' }], text); } else if (sectionId === 'contributors-section') { @@ -19958,6 +20068,7 @@

var _reviewQueueLimit = REVIEW_QUEUE_PAGE_SIZE; var _reviewQueueTotal = 0; var _reviewQueueHasMore = false; + window._reviewQueueHistory = window._reviewQueueHistory || []; // reviewQueuePriorityMeta maps the API's priority (high|normal|low) to the // review-priority/* label family the backend mirrors onto PRs; the badge @@ -20016,6 +20127,7 @@ _reviewQueueLimit = Number(data && data.limit) || REVIEW_QUEUE_PAGE_SIZE; _reviewQueueOffset = Number(data && data.offset) || 0; _reviewQueueHasMore = !!(data && data.has_more); + window._reviewQueueHistory = capDashboardArray((data && Array.isArray(data.history)) ? data.history : window._reviewQueueHistory, DASHBOARD_SERVER_HISTORY_CAP); var countEl = document.getElementById('review-queue-count'); if (countEl) countEl.textContent = _reviewQueueTotal ? _reviewQueueTotal + ' PR' + (_reviewQueueTotal === 1 ? '' : 's') : ''; setSectionSummary('review-queue-section', _reviewQueueTotal ? _reviewQueueTotal + ' PR' + (_reviewQueueTotal === 1 ? '' : 's') : '0 PRs'); From 2e696851345818a79fedd80e3684d65dfbf85464 Mon Sep 17 00:00:00 2001 From: clubanderson <407614+clubanderson@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:40:52 +0000 Subject: [PATCH 2/5] changelog: fragment for #10613 Generated by changelog-fragment-guard from the PR title (#5675). Edit the wording freely, or replace this file with the no-changelog label if the change is not user-visible. Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Signed-off-by: clubanderson <407614+clubanderson@users.noreply.github.com> --- changelog.d/added-10613.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/added-10613.md diff --git a/changelog.d/added-10613.md b/changelog.d/added-10613.md new file mode 100644 index 0000000000..fc7f5aadcf --- /dev/null +++ b/changelog.d/added-10613.md @@ -0,0 +1 @@ +- dashboard: Review Queue collapsed row shows a priority-split history sparkline (#10613) From a519214139da90328b7005de18b6d494381a6aa1 Mon Sep 17 00:00:00 2001 From: Andy Anderson Date: Mon, 5 Oct 2026 13:14:29 -0400 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=90=9B=20dashboard:=20fix=20review=20?= =?UTF-8?q?queue=20summary=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andy Anderson --- changelog.d/fixed-10613-review-queue-summary.md | 1 + src/pkg/dashboard/static/index.html | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixed-10613-review-queue-summary.md diff --git a/changelog.d/fixed-10613-review-queue-summary.md b/changelog.d/fixed-10613-review-queue-summary.md new file mode 100644 index 0000000000..e5e7b8e25a --- /dev/null +++ b/changelog.d/fixed-10613-review-queue-summary.md @@ -0,0 +1 @@ +- Fixed the review queue collapsed summary escape helper used by visual summary tests. diff --git a/src/pkg/dashboard/static/index.html b/src/pkg/dashboard/static/index.html index 49f05322ea..180657ddfc 100644 --- a/src/pkg/dashboard/static/index.html +++ b/src/pkg/dashboard/static/index.html @@ -5559,7 +5559,7 @@

What sh }).join(' '); } function reviewQueueRangeControls() { - return `${Object.keys(REVIEW_QUEUE_WINDOWS).map(win => ``).join('')}`; + return `${Object.keys(REVIEW_QUEUE_WINDOWS).map(win => ``).join('')}`; } function reviewQueueSparkline(entries) { const total = reviewQueueSeries(entries, 'total'); @@ -5599,7 +5599,7 @@

What sh ['high', 'H', high], ['normal', 'N', normal], ['low', 'L', low], - ].map(([key, label, value]) => `${esc(label)} ${fmtSparkVal(value)}`).join(''); + ].map(([key, label, value]) => `${escapeHtml(label)} ${fmtSparkVal(value)}`).join(''); const label = `${fmtSparkVal(shownTotal)} PRs · high ${high}, normal ${normal}, low ${low}, ${deltaText} vs range start`; const inner = `${reviewQueueRangeControls()}${fmtSparkVal(shownTotal)}${chips}${deltaText}${reviewQueueSparkline(entries)}`; return `${inner}`; From f9326c8b09b69098f14ccf76962b4330daf0783f Mon Sep 17 00:00:00 2001 From: Andy Anderson Date: Mon, 5 Oct 2026 13:22:50 -0400 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9B=20dashboard:=20render=20review?= =?UTF-8?q?=20queue=20mini=20sparkline?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andy Anderson --- src/pkg/dashboard/static/index.html | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pkg/dashboard/static/index.html b/src/pkg/dashboard/static/index.html index 180657ddfc..3aade22b21 100644 --- a/src/pkg/dashboard/static/index.html +++ b/src/pkg/dashboard/static/index.html @@ -5561,12 +5561,13 @@

What sh function reviewQueueRangeControls() { return `${Object.keys(REVIEW_QUEUE_WINDOWS).map(win => ``).join('')}`; } - function reviewQueueSparkline(entries) { + function reviewQueueSparkline(entries, fallbackTotal) { const total = reviewQueueSeries(entries, 'total'); const high = reviewQueueSeries(entries, 'high'); const normal = reviewQueueSeries(entries, 'normal'); const low = reviewQueueSeries(entries, 'low'); - const values = total.length ? total : [0, 0]; + const fallback = Math.max(0, Number(fallbackTotal) || 0); + const values = total.length ? total : [fallback, fallback]; const max = Math.max(1, ...values, ...high, ...normal, ...low); const latest = values[values.length - 1] || 0; const min = Math.min(0, ...values); @@ -5579,17 +5580,18 @@

What sh return `${renderSparkline(null, values, { width: REVIEW_QUEUE_SPARK_W, height: REVIEW_QUEUE_SPARK_H, min, max, color: 'color-mix(in srgb, var(--purple) 60%, transparent)', title, ariaLabel: `Review Queue total: ${title}`, label: 'review queue total', id: 'review-queue-collapsed', - dot: false, strokeWidth: 1, after, wrapperClass: 'sparkline--flush', wrapperTitle: title, + dot: false, strokeWidth: 1, after, svgClass: 'mini-spark', wrapperClass: 'sparkline--flush', wrapperTitle: title, })}`; } function reviewQueueCollapsedSummary(text) { const entries = reviewQueueHistoryEntries(); const latest = entries[entries.length - 1] || {}; const total = Number(latest.total); + const shownTotal = Number.isFinite(total) ? total : miniFirstNumber(text); + const hasPrioritySplit = Number.isFinite(Number(latest.high)) || Number.isFinite(Number(latest.normal)) || Number.isFinite(Number(latest.low)); const high = Number(latest.high) || 0; - const normal = Number(latest.normal) || 0; + const normal = hasPrioritySplit ? (Number(latest.normal) || 0) : shownTotal; const low = Number(latest.low) || 0; - const shownTotal = Number.isFinite(total) ? total : miniFirstNumber(text); const first = entries[0] || {}; const startTotal = Number(first.total); const delta = Number.isFinite(startTotal) ? shownTotal - startTotal : 0; @@ -5601,7 +5603,7 @@

What sh ['low', 'L', low], ].map(([key, label, value]) => `${escapeHtml(label)} ${fmtSparkVal(value)}`).join(''); const label = `${fmtSparkVal(shownTotal)} PRs · high ${high}, normal ${normal}, low ${low}, ${deltaText} vs range start`; - const inner = `${reviewQueueRangeControls()}${fmtSparkVal(shownTotal)}${chips}${deltaText}${reviewQueueSparkline(entries)}`; + const inner = `${reviewQueueRangeControls()}${fmtSparkVal(shownTotal)}${chips}${deltaText}${reviewQueueSparkline(entries, shownTotal)}`; return `${inner}`; } function setReviewQueueWindow(win) { From 0c7075e1221337e97b1f3bfb8557d68a5acaad9a Mon Sep 17 00:00:00 2001 From: Andy Anderson Date: Mon, 5 Oct 2026 13:49:55 -0400 Subject: [PATCH 5/5] =?UTF-8?q?=F0=9F=90=9B=20dashboard:=20preserve=20revi?= =?UTF-8?q?ew=20queue=20sparkline=20wiring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andy Anderson --- src/pkg/dashboard/static/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pkg/dashboard/static/index.html b/src/pkg/dashboard/static/index.html index 3aade22b21..2cc1f37fbd 100644 --- a/src/pkg/dashboard/static/index.html +++ b/src/pkg/dashboard/static/index.html @@ -5561,13 +5561,12 @@

What sh function reviewQueueRangeControls() { return `${Object.keys(REVIEW_QUEUE_WINDOWS).map(win => ``).join('')}`; } - function reviewQueueSparkline(entries, fallbackTotal) { + function reviewQueueSparkline(entries) { const total = reviewQueueSeries(entries, 'total'); const high = reviewQueueSeries(entries, 'high'); const normal = reviewQueueSeries(entries, 'normal'); const low = reviewQueueSeries(entries, 'low'); - const fallback = Math.max(0, Number(fallbackTotal) || 0); - const values = total.length ? total : [fallback, fallback]; + const values = total.length ? total : [0, 0]; const max = Math.max(1, ...values, ...high, ...normal, ...low); const latest = values[values.length - 1] || 0; const min = Math.min(0, ...values); @@ -5603,7 +5602,8 @@

What sh ['low', 'L', low], ].map(([key, label, value]) => `${escapeHtml(label)} ${fmtSparkVal(value)}`).join(''); const label = `${fmtSparkVal(shownTotal)} PRs · high ${high}, normal ${normal}, low ${low}, ${deltaText} vs range start`; - const inner = `${reviewQueueRangeControls()}${fmtSparkVal(shownTotal)}${chips}${deltaText}${reviewQueueSparkline(entries, shownTotal)}`; + const sparkEntries = entries.length ? entries : [{ t: Date.now(), total: shownTotal, high, normal, low }]; + const inner = `${reviewQueueRangeControls()}${fmtSparkVal(shownTotal)}${chips}${deltaText}${reviewQueueSparkline(sparkEntries)}`; return `${inner}`; } function setReviewQueueWindow(win) {