ReviewOS

also looking at this

chris/very-good-adblock

refactor(safari): migrate the safari pipeline to the framework buddy commands

#3
Merged chris wants to merge chore/safari-framework-migration into main
4 files +28 -6
src/shared/metrics.tsmodified+12-0
Changes to src/shared/metrics.ts
@@ -82,6 +82,18 @@ export function compactBuckets(buckets: StatBucket[], limit: number): StatBucket
8282 return [...buckets].sort((a, b) => a.key.localeCompare(b.key)).slice(-limit)
8383}
8484
85/**
86 * YYYY-MM-DD in the user's local timezone. Daily buckets follow the local
87 * calendar so "Blocked today" rolls over at local midnight, and the chart's
88 * day labels (parsed as local dates) match the day the user experienced.
89 */
90export function localDayKey(date: Date = new Date()): string {
91 const year = date.getFullYear()
92 const month = String(date.getMonth() + 1).padStart(2, '0')
93 const day = String(date.getDate()).padStart(2, '0')
94 return `${year}-${month}-${day}`
95}
96
8597export function eventTotals(events: BlockEvent[]): Pick<StatBucket, 'adsBlocked' | 'bytesSaved' | 'videoSecondsSaved'> {
8698 return events.reduce(
8799 (totals, event) => {
src/shared/storage.tsmodified+5-3
Changes to src/shared/storage.ts
@@ -1,6 +1,6 @@
11import { maxRecentEvents } from './constants'
22import { hostnameFromUrl, isHttpUrl, normalizeHostname, siteMatches } from './domain'
3import { compactBuckets, eventTotals } from './metrics'
3import { compactBuckets, eventTotals, localDayKey } from './metrics'
44import type { ActiveTabState, BlockEvent, CloudStatsSnapshot, ExtensionSettings, LifetimeStats, LocalStats, SiteStats, StatBucket } from './types'
55
66const syncKeys = {
@@ -439,8 +439,10 @@ function lifetimeMatches(left: Partial<LifetimeStats> | undefined, right: Lifeti
439439}
440440
441441function bucketKey(date: Date, type: 'hour' | 'day'): string {
442 const iso = date.toISOString()
443 return type === 'hour' ? iso.slice(0, 13) : iso.slice(0, 10)
442 // Hours stay UTC (they're only ever shown relatively, "N hours ago"); days
443 // follow the local calendar so "today" means the user's own day.
444 if (type === 'day') return localDayKey(date)
445 return date.toISOString().slice(0, 13)
444446}
445447
446448function mergeBucket(target: LocalStats['hourly'], key: string, totals: { adsBlocked: number, bytesSaved: number, videoSecondsSaved: number }): void {
src/ui/options.tsmodified+2-2
Changes to src/ui/options.ts
@@ -1,5 +1,5 @@
11import { normalizeHostname } from '../shared/domain'
2import { formatBytes, formatMinutes } from '../shared/metrics'
2import { formatBytes, formatMinutes, localDayKey } from '../shared/metrics'
33import type { DashboardState, RuntimeMessage } from '../shared/types'
44import { byId, downloadJson, relativeTime, renderBars, sendMessage } from './dom'
55import { sourceLabel } from './labels'
@@ -187,7 +187,7 @@ function render(next: DashboardState): void {
187187 elements.blocked.textContent = next.lifetime.adsBlocked.toLocaleString()
188188 elements.data.textContent = formatBytes(next.lifetime.bytesSaved)
189189 elements.video.textContent = formatMinutes(next.lifetime.videoSecondsSaved)
190 const todayKey = new Date().toISOString().slice(0, 10)
190 const todayKey = localDayKey()
191191 const today = next.local.daily.find(bucket => bucket.key === todayKey)
192192 elements.today.textContent = (today?.adsBlocked ?? 0).toLocaleString()
193193 elements.enabled.checked = next.settings.enabled
test/metrics.test.tsmodified+9-1
Changes to test/metrics.test.ts
@@ -1,5 +1,5 @@
11import { describe, expect, it } from 'bun:test'
2import { categoryForRequestType, estimateBytesSaved, estimateVideoAdBytes, estimateVideoSecondsSaved, eventTotals, formatBytes, formatMinutes } from '../src/shared/metrics'
2import { categoryForRequestType, estimateBytesSaved, estimateVideoAdBytes, estimateVideoSecondsSaved, eventTotals, formatBytes, formatMinutes, localDayKey } from '../src/shared/metrics'
33
44describe('metrics', () => {
55 it('formats estimated savings', () => {
@@ -32,6 +32,14 @@ describe('metrics', () => {
3232 expect(categoryForRequestType('something-else')).toBe('other')
3333 })
3434
35 it('keys days by the local calendar, not UTC', () => {
36 // 23:30 local must stay on the local day regardless of the host timezone —
37 // this is what makes "Blocked today" roll over at local midnight.
38 expect(localDayKey(new Date(2026, 6, 5, 23, 30))).toBe('2026-07-05')
39 expect(localDayKey(new Date(2026, 0, 1, 0, 30))).toBe('2026-01-01')
40 expect(localDayKey()).toMatch(/^\d{4}-\d{2}-\d{2}$/)
41 })
42
3543 it('rolls up block events', () => {
3644 const totals = eventTotals([
3745 {