Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,10 @@ indexer, and health reports 503.

Cloud archive rows through September 22, 2026 retain the original values,
including captured PHA. New rows use `source='api'` for the just-closed day and
`source='api-backfill'` for older missing days. The Cloud API recomputes old
running statistics and user counts using current VM/user status, so historical
gaps understate running VMs. The API exposes `backfilled: true` on those rows.
`source='api-backfill'` for older missing days. The Cloud API recomputes past
days from each VM's and user's current status, so those days understate running
VMs and users. The API marks them `backfilled: true`; the page draws them like
any other day.
`running_vcpu` and `active_users` are captured when available; archive rows
have null values for both.

Expand Down Expand Up @@ -148,7 +149,7 @@ Vault rewards are summed per ISO week (Monday to Sunday UTC) of their
`flowDate` and drawn as bars. The latest week appears once its Sunday has a
row; the data table lists each day, including the current week.
Sunset claims are shown as totals. APR is hidden while the latest Vault row is
unreviewed. Backfilled Cloud days are shaded in the Users and VMs charts.
unreviewed.

All assets, including the Geist font, are bundled; nothing loads from a CDN.
`public/_headers` sets the CSP on static assets (Worker responses are
Expand Down
29 changes: 5 additions & 24 deletions app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,11 @@ import {
dayTime,
formatCompact,
formatDay,
formatDayRange,
formatNumber,
formatPercent,
formatTimestamp,
} from '@/lib/format'
import {
backfillRanges,
latest,
type Row,
type Series,
Expand Down Expand Up @@ -126,7 +124,6 @@ const derive = (data: AnalyticsData) => {
vcpu: cloud('totalVcpu'),
captured: cloud('captured'),
},
shaded: backfillRanges(data.cloud),
rows: {
circulation: toRows(
data.circulation,
Expand Down Expand Up @@ -199,9 +196,11 @@ export function App() {

return (
<>
{/* SiteHeader from dashboard-01; the links follow its GitHub button. */}
{/* SiteHeader from dashboard-01; the links follow its GitHub button.
Its row and main share one page width, as on the shadcn/ui site
(max-w-(--breakpoint-2xl)). */}
<header className="flex h-12 shrink-0 items-center gap-2 border-b">
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
<div className="mx-auto flex w-full max-w-(--breakpoint-2xl) items-center gap-1 px-4 lg:gap-2 lg:px-6">
<PhalaLogo className="h-6 w-auto" />
<Separator
orientation="vertical"
Expand All @@ -226,7 +225,7 @@ export function App() {
</header>

<main
className="@container/main flex flex-1 flex-col gap-2"
className="@container/main mx-auto flex w-full max-w-(--breakpoint-2xl) flex-1 flex-col gap-2"
aria-busy={state.status === 'loading'}
>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
Expand Down Expand Up @@ -420,16 +419,12 @@ export function App() {
description="Daily total"
rows={view?.rows.cloud}
series={USERS}
shaded={view?.shaded}
shadedLabel="recomputed"
/>
<ChartCard
title="VMs"
description="Total and running, daily"
rows={view?.rows.cloud}
series={VMS}
shaded={view?.shaded}
shadedLabel="recomputed"
/>
<ChartCard
title="Total vCPUs"
Expand All @@ -444,20 +439,6 @@ export function App() {
series={REVENUE}
/>
</div>
{view && view.shaded.length > 0 && (
<div className="px-4 lg:px-6">
<Alert role="note">
<InfoIcon />
<AlertTitle>Backfilled days are shaded</AlertTitle>
<AlertDescription>
{view.shaded.map(formatDayRange).join(', ')}: Phala Cloud
recomputes past days using each VM's and user's current
status, so running VMs and users are understated for those
days.
</AlertDescription>
</Alert>
</div>
)}
</Section>

<footer className="px-4 lg:px-6">
Expand Down
21 changes: 1 addition & 20 deletions app/src/components/chart-area.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import {useId} from 'react'
import {
Area,
AreaChart,
CartesianGrid,
ReferenceArea,
XAxis,
YAxis,
} from 'recharts'
import {Area, AreaChart, CartesianGrid, XAxis, YAxis} from 'recharts'
import {
type ChartConfig,
ChartContainer,
Expand Down Expand Up @@ -43,14 +36,11 @@ export default function ChartArea({
title,
rows,
series,
shaded = [],
}: {
title: string
rows: Row[]
/** Drawn series; colors are theme variables such as `var(--chart-1)`. */
series: (Series & {color: string})[]
/** Inclusive [first, last] UTC days to shade. */
shaded?: [string, string][]
}) {
const id = useId().replaceAll(':', '')
const chartConfig = Object.fromEntries(
Expand Down Expand Up @@ -105,15 +95,6 @@ export default function ChartArea({
))}
</defs>
<CartesianGrid vertical={false} />
{shaded.map(([x1, x2]) => (
<ReferenceArea
key={x1}
x1={x1}
x2={x2}
fill="var(--muted-foreground)"
fillOpacity={0.15}
/>
))}
<XAxis
dataKey="date"
tickLine={false}
Expand Down
19 changes: 1 addition & 18 deletions app/src/components/chart-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export function ChartCard({
description,
rows,
series,
shaded = [],
shadedLabel,
bars,
table,
footer,
Expand All @@ -54,9 +52,6 @@ export function ChartCard({
rows: Row[] | undefined
/** Series with a color are drawn; the others appear in the table only. */
series: Series[]
shaded?: [string, string][]
/** Table marker for rows inside `shaded`. */
shadedLabel?: string
/** Draw the first colored series as bars; the tooltip prefixes each row's date. */
bars?: {periodLabel: string}
table?: {rows: Row[] | undefined; series: Series[]}
Expand All @@ -67,8 +62,6 @@ export function ChartCard({
)
const tableRows = table ? table.rows : rows
const tableSeries = table ? table.series : series
const isShaded = (date: string) =>
shaded.some(([first, last]) => first <= date && date <= last)
const skeleton = <Skeleton className="h-[250px] w-full" />

return (
Expand All @@ -89,12 +82,7 @@ export function ChartCard({
periodLabel={bars.periodLabel}
/>
) : rows ? (
<ChartArea
title={title}
rows={rows}
series={drawn}
shaded={shaded}
/>
<ChartArea title={title} rows={rows} series={drawn} />
) : (
skeleton
)}
Expand Down Expand Up @@ -131,11 +119,6 @@ export function ChartCard({
<TableRow key={row.date}>
<TableCell className="font-medium">
{formatDay(row.date)}
{shadedLabel && isShaded(row.date) && (
<span className="text-muted-foreground">
{` (${shadedLabel})`}
</span>
)}
</TableCell>
{tableSeries.map(({key}) => (
<TableCell key={key} className="text-right">
Expand Down
11 changes: 3 additions & 8 deletions app/src/lib/format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,14 @@ import {expect, test} from 'bun:test'

// Far from UTC, so local-time formatting would shift these dates.
process.env.TZ = 'Pacific/Kiritimati'
const {formatDay, formatDayRange, formatMonth, formatNumber, formatTimestamp} =
await import('./format')
const {formatDay, formatMonth, formatNumber, formatTimestamp} = await import(
'./format'
)

test('dates are formatted as UTC days regardless of the reader time zone', () => {
expect(formatDay('2026-10-28')).toContain('28')
expect(formatTimestamp('2026-10-28T23:30:00Z')).toMatch(/28.*UTC$/)
expect(formatTimestamp('2026-10-28T23:30:00Z')).not.toContain('29')
expect(formatDayRange(['2026-09-23', '2026-09-23'])).toBe(
formatDay('2026-09-23'),
)
expect(formatDayRange(['2026-09-23', '2026-10-26'])).toBe(
`${formatDay('2026-09-23')} – ${formatDay('2026-10-26')}`,
)
// Axis labels for January 1 stay in that month and year.
expect(formatMonth('2025-01-01')).toContain('2025')
expect(formatMonth('2025-01-01')).toBe(
Expand Down
2 changes: 0 additions & 2 deletions app/src/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@ export const formatPercent = (value: number) => percent.format(value)
/** Milliseconds at 00:00 UTC on a YYYY-MM-DD date. */
export const dayTime = (date: string) => Date.parse(`${date}T00:00:00Z`)
export const formatDay = (date: string) => day.format(dayTime(date))
export const formatDayRange = ([first, last]: readonly [string, string]) =>
first === last ? formatDay(first) : `${formatDay(first)} – ${formatDay(last)}`
/** A recent ISO timestamp as a UTC date and time, e.g. "Oct 28, 2:27 PM UTC". */
export const formatTimestamp = (iso: string) =>
`${dateTime.format(Date.parse(iso))} UTC`
Expand Down
18 changes: 1 addition & 17 deletions app/src/lib/series.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {AnalyticsData} from '../../worker/data'
import {vaultDay} from '../../worker/fixtures'
import {parseDay, VAULT_FIELDS} from '../../worker/subgraph'
import {DAY_MS} from './format'
import {backfillRanges, weeklyRewards} from './series'
import {weeklyRewards} from './series'

const vault = (flowDates: string[], rewards: number[]) =>
flowDates.map((flowDate, index) => ({
Expand Down Expand Up @@ -46,19 +46,3 @@ test('quiet days count as 0 and a week of quiet days stays a 0 bar', () => {
['2026-10-26', 0],
])
})

test('backfilled Cloud days group into consecutive ranges', () => {
const cloud = [
['2026-09-22', false],
['2026-09-23', true],
['2026-09-24', true],
['2026-09-25', false],
['2026-09-26', true],
['2026-09-28', true],
].map(([date, backfilled]) => ({date, backfilled})) as AnalyticsData['cloud']
expect(backfillRanges(cloud)).toEqual([
['2026-09-23', '2026-09-24'],
['2026-09-26', '2026-09-26'],
['2026-09-28', '2026-09-28'],
])
})
14 changes: 0 additions & 14 deletions app/src/lib/series.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,6 @@ export const weeklyRewards = (vault: AnalyticsData['vault']): Row[] => {
}))
}

/** Consecutive backfilled Cloud days as inclusive [first, last] date ranges. */
export const backfillRanges = (
cloud: AnalyticsData['cloud'],
): [string, string][] => {
const ranges: [string, string][] = []
for (const {date, backfilled} of cloud) {
if (!backfilled) continue
const last = ranges.at(-1)
if (last && dayTime(date) - dayTime(last[1]) === DAY_MS) last[1] = date
else ranges.push([date, date])
}
return ranges
}

/** Latest row with a non-null value for `key`. */
export const latest = <T, K extends keyof T>(rows: readonly T[], key: K) =>
rows.findLast((row) => row[key] != null)
Loading