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' });
}
// 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(() => {
const printed = badge_li.filter((b) => (b.print_count ?? 0) >= 1 && b.print_last_datetime);
if (!printed.length) {
@@ -41,7 +49,7 @@ let stats: PrintStats = $derived.by(() => {
const bucket_ms = bucket_size * 60 * 1000;
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));
if (!times.length) {
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 end = Math.ceil((max_raw + 1) / bucket_ms) * bucket_ms;
const buckets: Bucket[] = [];
let prev_day = '';
const raw_buckets: Omit<Bucket, 'date_label'>[] = [];
for (let t = start; t < end; t += bucket_ms) {
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;
});
if (in_bucket.length === 0) continue;
const day = fmt_date(t);
const date_label = day !== prev_day ? day : null;
prev_day = day;
buckets.push({
raw_buckets.push({
start_ms: t,
label: fmt_time(t),
date_label,
count: in_bucket.length,
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 span_label =
buckets.length > 0