feat: integrate AI growth analyst experience

This commit is contained in:
2026-08-21 01:16:57 +03:30
parent 7a495b04c2
commit c79a1219bf
12 changed files with 2441 additions and 1421 deletions

View File

@@ -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,

View 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

File diff suppressed because it is too large Load Diff