feat: implement v0.4.0 - Reports, Charts, Comparison, Dark Mode, E2E Testing
Some checks failed
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled

Backend (@backend-dev):
- Add ReportService with PDF/CSV generation (reportlab, pandas)
- Implement Report API endpoints (POST, GET, DELETE, download)
- Add ReportRepository and schemas
- Configure storage with auto-cleanup (30 days)
- Rate limiting: 10 downloads/minute
- Professional PDF templates with charts support

Frontend (@frontend-dev):
- Integrate Recharts for data visualization
- Add CostBreakdown, TimeSeries, ComparisonBar charts
- Implement scenario comparison page with multi-select
- Add dark/light mode toggle with ThemeProvider
- Create Reports page with generation form and list
- Add new UI components: checkbox, dialog, tabs, label, skeleton
- Implement useComparison and useReports hooks

QA (@qa-engineer):
- Setup Playwright E2E testing framework
- Create 7 test spec files with 94 test cases
- Add visual regression testing with baselines
- Configure multi-browser testing (Chrome, Firefox, WebKit)
- Add mobile responsive tests
- Create test fixtures and helpers
- Setup GitHub Actions CI workflow

Documentation (@spec-architect):
- Create detailed kanban-v0.4.0.md with 27 tasks
- Update progress.md with v0.4.0 tracking
- Create v0.4.0 planning prompt

Features:
 PDF/CSV Report Generation
 Interactive Charts (Pie, Area, Bar)
 Scenario Comparison (2-4 scenarios)
 Dark/Light Mode Toggle
 E2E Test Suite (94 tests)

Dependencies added:
- Backend: reportlab, pandas, slowapi
- Frontend: recharts, date-fns, @radix-ui/react-checkbox/dialog/tabs
- Testing: @playwright/test

27 tasks completed, 100% v0.4.0 implementation
This commit is contained in:
Luca Sacchi Ricciardi
2026-04-07 16:11:47 +02:00
parent 311a576f40
commit a5fc85897b
63 changed files with 9218 additions and 246 deletions

View File

@@ -0,0 +1,43 @@
import { useMutation, useQuery } from '@tanstack/react-query';
import api from '@/lib/api';
import type { Scenario, MetricSummary } from '@/types/api';
const COMPARISON_KEY = 'comparison';
export interface ComparisonScenario {
scenario: Scenario;
summary: MetricSummary;
}
export interface ComparisonResult {
scenarios: ComparisonScenario[];
deltas: Record<string, { value: number; percentage: number }[]>;
}
export interface CompareRequest {
scenario_ids: string[];
metrics?: string[];
}
export function useCompareScenarios() {
return useMutation<ComparisonResult, Error, CompareRequest>({
mutationFn: async (data) => {
const response = await api.post('/scenarios/compare', data);
return response.data;
},
});
}
export function useComparisonCache(scenarioIds: string[]) {
return useQuery<ComparisonResult>({
queryKey: [COMPARISON_KEY, scenarioIds.sort().join(',')],
queryFn: async () => {
const response = await api.post('/scenarios/compare', {
scenario_ids: scenarioIds,
});
return response.data;
},
enabled: scenarioIds.length >= 2 && scenarioIds.length <= 4,
staleTime: 5 * 60 * 1000, // 5 minutes cache
});
}

View File

@@ -0,0 +1,118 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import api from '@/lib/api';
const REPORTS_KEY = 'reports';
export type ReportFormat = 'pdf' | 'csv';
export type ReportStatus = 'pending' | 'processing' | 'completed' | 'failed';
export type ReportSection = 'summary' | 'costs' | 'metrics' | 'logs' | 'pii';
export interface Report {
id: string;
scenario_id: string;
format: ReportFormat;
status: ReportStatus;
created_at: string;
completed_at?: string;
file_size?: number;
file_path?: string;
error_message?: string;
sections: ReportSection[];
date_from?: string;
date_to?: string;
}
export interface ReportList {
items: Report[];
total: number;
}
export interface GenerateReportRequest {
format: ReportFormat;
include_logs?: boolean;
date_from?: string;
date_to?: string;
sections: ReportSection[];
}
export function useReports(scenarioId: string) {
return useQuery<ReportList>({
queryKey: [REPORTS_KEY, scenarioId],
queryFn: async () => {
const response = await api.get(`/scenarios/${scenarioId}/reports`);
return response.data;
},
enabled: !!scenarioId,
});
}
export function useReport(reportId: string) {
return useQuery<Report>({
queryKey: [REPORTS_KEY, 'detail', reportId],
queryFn: async () => {
const response = await api.get(`/reports/${reportId}`);
return response.data;
},
enabled: !!reportId,
});
}
export function useGenerateReport(scenarioId: string) {
const queryClient = useQueryClient();
return useMutation<Report, Error, GenerateReportRequest>({
mutationFn: async (data) => {
const response = await api.post(`/scenarios/${scenarioId}/reports`, data);
return response.data;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [REPORTS_KEY, scenarioId] });
},
});
}
export function useDownloadReport() {
return useMutation<Blob, Error, { reportId: string; fileName: string }>({
mutationFn: async ({ reportId }) => {
const response = await api.get(`/reports/${reportId}/download`, {
responseType: 'blob',
});
return response.data;
},
});
}
export function useDeleteReport(scenarioId: string) {
const queryClient = useQueryClient();
return useMutation<void, Error, string>({
mutationFn: async (reportId) => {
await api.delete(`/reports/${reportId}`);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [REPORTS_KEY, scenarioId] });
},
});
}
export function formatFileSize(bytes?: number): string {
if (!bytes) return '-';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}
export function getStatusBadgeVariant(status: ReportStatus): 'default' | 'secondary' | 'destructive' | 'outline' {
switch (status) {
case 'completed':
return 'default';
case 'processing':
return 'secondary';
case 'failed':
return 'destructive';
case 'pending':
return 'outline';
default:
return 'default';
}
}