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
18 changes: 16 additions & 2 deletions apps/edge/src/edge.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, expect, it } from 'vitest';
import { Wallet, TypedDataEncoder } from 'ethers';
import { buildDeployAuthorizationTypedData } from '@apogee/core';
import { buildEdgeServer, skillInvokeBootstrapComplete, skillInvokeCanRunAfterBootstrap } from './index.js';
import type { BillingChainClient, StorageBoundary } from '@apogee/billing';
import { buildEdgeServer, buildReceiptHeatmapCells, skillInvokeBootstrapComplete, skillInvokeCanRunAfterBootstrap } from './index.js';
import type { BillingChainClient, ReceiptIndexRow, StorageBoundary } from '@apogee/billing';

const address = '0x0000000000000000000000000000000000000001';
const txHash = '0x'.padEnd(66, '1');
Expand Down Expand Up @@ -57,6 +57,20 @@ describe('edge API', () => {
expect(skillInvokeCanRunAfterBootstrap('activating', false, 'active')).toBe(true);
});

it('buckets receipt heatmap by UTC calendar day and hour', () => {
const now = Date.parse('2026-05-22T10:30:00.000Z');
const rows: ReceiptIndexRow[] = [
{ receiptId: 'r1', agentId: '1', actionTag: 'text.summarize', payloadHash: '0x1', storageRoot: '0x2', valueWei: '0', txHash, status: 'minted', createdAt: '2026-05-21T23:15:00.000Z' },
{ receiptId: 'r2', agentId: '1', actionTag: 'text.summarize', payloadHash: '0x3', storageRoot: '0x4', valueWei: '0', txHash, status: 'minted', createdAt: '2026-05-22T09:00:00.000Z' },
{ receiptId: 'r3', agentId: '1', actionTag: 'text.summarize', payloadHash: '0x5', storageRoot: '0x6', valueWei: '0', txHash, status: 'minted', createdAt: '2026-05-15T23:59:00.000Z' },
];

expect(buildReceiptHeatmapCells(rows, 7, now)).toEqual([
{ day: 5, hour: 23, count: 1 },
{ day: 6, hour: 9, count: 1 },
]);
});

it('serves health, docs, quote, settle, auth-gated agent, memory, receipt, and refund routes', async () => {
const app = buildEdgeServer({ chainClient: new FakeChain(), storageClient: new FakeStorage(), signerKey, chainId: 16602, paymentRouterAddress: address, receiptBookAddress: address, jwtSecret: 'test-secret' });
await app.ready();
Expand Down
41 changes: 26 additions & 15 deletions apps/edge/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,31 @@ const problem = (reply: FastifyReply, status: number, title: string, detail: str
};
const sameAddress = (a: string, b: string): boolean => a.toLowerCase() === b.toLowerCase();

export function buildReceiptHeatmapCells(rows: ReceiptIndexRow[], days: number, nowMs = Date.now()): Array<{ day: number; hour: number; count: number }> {
const now = new Date(nowMs);
const todayUtc = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const dayIndexByDate = new Map<string, number>();
for (let dayIndex = 0; dayIndex < days; dayIndex += 1) {
const date = new Date(todayUtc - (days - 1 - dayIndex) * 86_400_000).toISOString().slice(0, 10);
dayIndexByDate.set(date, dayIndex);
}

const cells = new Map<string, { day: number; hour: number; count: number }>();
for (const receipt of rows) {
const created = new Date(receipt.createdAt);
if (!Number.isFinite(created.getTime())) continue;
const day = dayIndexByDate.get(created.toISOString().slice(0, 10));
if (day === undefined) continue;
const hour = created.getUTCHours();
const key = `${day}:${hour}`;
const cell = cells.get(key) ?? { day, hour, count: 0 };
cell.count += 1;
cells.set(key, cell);
}

return [...cells.values()].sort((a, b) => a.day - b.day || a.hour - b.hour);
}

async function requireAuth(request: FastifyRequest): Promise<AuthUser> {
await request.jwtVerify();
return { address: getAddress(request.user.address) };
Expand Down Expand Up @@ -1680,21 +1705,7 @@ export function buildEdgeServer(options: EdgeServerOptions): FastifyInstance {
const ownedAgentIds = new Set([...store.agents.values()].filter((agent) => sameAddress(agent.owner, user.address)).map((agent) => agent.id));
rows = rows.filter((receipt) => ownedAgentIds.has(receipt.agentId));
}
const cells = new Map<string, { day: number; hour: number; count: number }>();
const now = Date.now();
for (const receipt of rows) {
const created = new Date(receipt.createdAt).getTime();
if (!Number.isFinite(created)) continue;
const ageDays = Math.floor((now - created) / 86_400_000);
if (ageDays < 0 || ageDays >= query.days) continue;
const day = query.days - 1 - ageDays;
const hour = new Date(receipt.createdAt).getHours();
const key = `${day}:${hour}`;
const cell = cells.get(key) ?? { day, hour, count: 0 };
cell.count += 1;
cells.set(key, cell);
}
return [...cells.values()].sort((a, b) => a.day - b.day || a.hour - b.hour);
return buildReceiptHeatmapCells(rows, query.days);
});

// ── Public proofs data (no auth, ISR-friendly) ────────────────────────────
Expand Down
12 changes: 7 additions & 5 deletions apps/web/src/components/dashboard/DashboardHeatmap.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import React from 'react';
import type { HeatmapCell } from '@/lib/types';

const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const HOURS = Array.from({ length: 24 }, (_, i) => i);

// Edge returns day 0 = oldest, day 6 = today. Map each index to an actual date.
// Edge returns UTC calendar day 0 = oldest, day 6 = today. Keep labels in
// the same UTC frame as /v1/receipts/heatmap and /v1/proofs bucketing.
function buildDayLabels(): { short: string; full: string }[] {
const today = new Date();
const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(today);
d.setDate(today.getDate() - (6 - i));
const d = new Date(todayUtc - (6 - i) * 86_400_000);
return {
short: `${WEEKDAYS[d.getDay()]} ${d.getDate()}`,
full: d.toDateString(),
short: `${WEEKDAYS[d.getUTCDay()]} ${d.getUTCDate()}`,
full: d.toISOString().slice(0, 10),
};
});
}
Expand Down
Loading