feat: integrate AI growth analyst experience
This commit is contained in:
@@ -11,6 +11,7 @@ export type SessionStatus = components['schemas']['SessionStatus'];
|
||||
export type DetailView = components['schemas']['DetailView'];
|
||||
export type FailureStage = components['schemas']['FailureStage'];
|
||||
export type Problem = components['schemas']['Problem'];
|
||||
export type GrowthReport = components['schemas']['GrowthReport'];
|
||||
|
||||
const apiBase = (import.meta.env.PUBLIC_API_BASE_URL || '/api/v1').replace(/\/$/, '');
|
||||
|
||||
@@ -119,6 +120,23 @@ export const api = {
|
||||
sessionDetail(sessionId: string) {
|
||||
return request<SessionDetail>(`/analytics/sessions/${encodeURIComponent(sessionId)}`);
|
||||
},
|
||||
latestGrowthReport(terminalId?: string) {
|
||||
const query = terminalId ? `?terminalId=${encodeURIComponent(terminalId)}` : '';
|
||||
return request<GrowthReport>(`/analytics/growth-reports/latest${query}`);
|
||||
},
|
||||
createGrowthReport(csrfToken: string, options: { terminalId?: string; refresh?: boolean } = {}) {
|
||||
return request<GrowthReport>('/analytics/growth-reports', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-Token': csrfToken,
|
||||
'Idempotency-Key': crypto.randomUUID(),
|
||||
},
|
||||
body: JSON.stringify(options),
|
||||
});
|
||||
},
|
||||
growthReport(reportId: string) {
|
||||
return request<GrowthReport>(`/analytics/growth-reports/${encodeURIComponent(reportId)}`);
|
||||
},
|
||||
exportUrl(
|
||||
periodId: string,
|
||||
view: DetailView,
|
||||
|
||||
56
src/lib/api/growth-client.test.ts
Normal file
56
src/lib/api/growth-client.test.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { api } from './client';
|
||||
|
||||
const report = {
|
||||
reportId: 'report-1',
|
||||
status: 'QUEUED',
|
||||
scope: { terminalId: null },
|
||||
horizonDays: 90,
|
||||
deterministicEvidence: {},
|
||||
cached: false,
|
||||
stale: false,
|
||||
nextRefreshAt: '2026-08-22T00:00:00Z',
|
||||
createdAt: '2026-08-21T00:00:00Z',
|
||||
updatedAt: '2026-08-21T00:00:00Z',
|
||||
eventsUrl: '/api/v1/analytics/growth-reports/report-1/events',
|
||||
};
|
||||
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
describe('growth report API client', () => {
|
||||
it('reads the cached terminal report without creating a new report', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(report), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await api.latestGrowthReport('terminal-1');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledOnce();
|
||||
expect(fetchMock.mock.calls[0][0]).toBe('/api/v1/analytics/growth-reports/latest?terminalId=terminal-1');
|
||||
expect(fetchMock.mock.calls[0][1].method).toBeUndefined();
|
||||
});
|
||||
|
||||
it('marks manual refresh explicitly and sends the authenticated write headers', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue(
|
||||
new Response(JSON.stringify(report), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
vi.stubGlobal('crypto', { randomUUID: () => 'idempotency-1' });
|
||||
|
||||
await api.createGrowthReport('csrf-1', { terminalId: 'terminal-1', refresh: true });
|
||||
|
||||
const init = fetchMock.mock.calls[0][1];
|
||||
expect(init.method).toBe('POST');
|
||||
expect(init.headers['X-CSRF-Token']).toBe('csrf-1');
|
||||
expect(init.headers['Idempotency-Key']).toBe('idempotency-1');
|
||||
expect(JSON.parse(init.body)).toEqual({ terminalId: 'terminal-1', refresh: true });
|
||||
});
|
||||
});
|
||||
3236
src/lib/api/schema.d.ts
vendored
3236
src/lib/api/schema.d.ts
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user