fix(badges): print throughput report — descending sort + UTC timezone fix

Buckets now display newest-first. Naive UTC datetime strings from the
backend are normalized with a Z suffix before parsing so times display
in local browser timezone, matching the badge list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-06-09 11:16:14 -04:00
parent 868b4017f2
commit 1c541cd090

View File

@@ -33,6 +33,14 @@ function fmt_date(ms: number): string {
return new Date(ms).toLocaleDateString([], { month: 'short', day: 'numeric' }); return new Date(ms).toLocaleDateString([], { month: 'short', day: 'numeric' });
} }
// Naive UTC strings from the backend have no timezone indicator — append Z so
// the browser parses them as UTC and converts to local time, matching the badge list display.
function parse_utc_ms(dt: string | null | undefined): number {
if (!dt) return NaN;
const normalized = dt.match(/Z$|[+-]\d{2}:?\d{2}$/) ? dt : dt + 'Z';
return new Date(normalized).getTime();
}
let stats: PrintStats = $derived.by(() => { let stats: PrintStats = $derived.by(() => {
const printed = badge_li.filter((b) => (b.print_count ?? 0) >= 1 && b.print_last_datetime); const printed = badge_li.filter((b) => (b.print_count ?? 0) >= 1 && b.print_last_datetime);
if (!printed.length) { if (!printed.length) {
@@ -41,7 +49,7 @@ let stats: PrintStats = $derived.by(() => {
const bucket_ms = bucket_size * 60 * 1000; const bucket_ms = bucket_size * 60 * 1000;
const times = printed const times = printed
.map((b) => new Date(b.print_last_datetime).getTime()) .map((b) => parse_utc_ms(b.print_last_datetime))
.filter((t) => !isNaN(t)); .filter((t) => !isNaN(t));
if (!times.length) { if (!times.length) {
return { buckets: [], total_printed: printed.length, max_count: 1, span_label: '' }; return { buckets: [], total_printed: printed.length, max_count: 1, span_label: '' };
@@ -52,29 +60,33 @@ let stats: PrintStats = $derived.by(() => {
const start = Math.floor(min_raw / bucket_ms) * bucket_ms; const start = Math.floor(min_raw / bucket_ms) * bucket_ms;
const end = Math.ceil((max_raw + 1) / bucket_ms) * bucket_ms; const end = Math.ceil((max_raw + 1) / bucket_ms) * bucket_ms;
const buckets: Bucket[] = []; const raw_buckets: Omit<Bucket, 'date_label'>[] = [];
let prev_day = '';
for (let t = start; t < end; t += bucket_ms) { for (let t = start; t < end; t += bucket_ms) {
const in_bucket = printed.filter((b) => { const in_bucket = printed.filter((b) => {
const bt = new Date(b.print_last_datetime).getTime(); const bt = parse_utc_ms(b.print_last_datetime);
return bt >= t && bt < t + bucket_ms; return bt >= t && bt < t + bucket_ms;
}); });
if (in_bucket.length === 0) continue; if (in_bucket.length === 0) continue;
const day = fmt_date(t); raw_buckets.push({
const date_label = day !== prev_day ? day : null;
prev_day = day;
buckets.push({
start_ms: t, start_ms: t,
label: fmt_time(t), label: fmt_time(t),
date_label,
count: in_bucket.length, count: in_bucket.length,
badges: in_bucket badges: in_bucket
}); });
} }
// Newest-first display; re-assign date separators after reversing.
raw_buckets.reverse();
let prev_day = '';
const buckets: Bucket[] = raw_buckets.map((b) => {
const day = fmt_date(b.start_ms);
const date_label = day !== prev_day ? day : null;
prev_day = day;
return { ...b, date_label };
});
const max_count = Math.max(...buckets.map((b) => b.count), 1); const max_count = Math.max(...buckets.map((b) => b.count), 1);
const span_label = const span_label =
buckets.length > 0 buckets.length > 0