feat: implement v0.4.0 - Reports, Charts, Comparison, Dark Mode, E2E Testing
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
@@ -0,0 +1,227 @@
import {
AreaChart,
Area,
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { format } from 'date-fns';
import { formatCurrency, formatNumber } from './ChartContainer';
interface TimeSeriesDataPoint {
timestamp: string;
[key: string]: string | number;
}
interface TimeSeriesChartProps {
data: TimeSeriesDataPoint[];
series: Array<{
key: string;
name: string;
color: string;
type?: 'line' | 'area';
}>;
title?: string;
description?: string;
yAxisFormatter?: (value: number) => string;
chartType?: 'line' | 'area';
}
export function TimeSeriesChart({
data,
series,
title = 'Metrics Over Time',
description,
yAxisFormatter = formatNumber,
chartType = 'area',
}: TimeSeriesChartProps) {
const formatXAxis = (timestamp: string) => {
try {
const date = new Date(timestamp);
return format(date, 'MMM dd HH:mm');
} catch {
return timestamp;
}
};
const CustomTooltip = ({ active, payload, label }: {
active?: boolean;
payload?: Array<{ name: string; value: number; color: string }>;
label?: string;
}) => {
if (active && payload && payload.length) {
return (
<div className="rounded-lg border bg-popover p-3 shadow-md">
<p className="font-medium text-popover-foreground mb-2">
{label ? formatXAxis(label) : ''}
</p>
<div className="space-y-1">
{payload.map((entry) => (
<p key={entry.name} className="text-sm text-muted-foreground flex items-center gap-2">
<span
className="h-2 w-2 rounded-full"
style={{ backgroundColor: entry.color }}
/>
{entry.name}: {yAxisFormatter(entry.value)}
</p>
))}
</div>
</div>
);
}
return null;
};
const ChartComponent = chartType === 'area' ? AreaChart : LineChart;
return (
<Card className="w-full">
<CardHeader className="pb-2">
<CardTitle className="text-lg font-semibold">{title}</CardTitle>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</CardHeader>
<CardContent>
<div className="h-[350px]">
<ResponsiveContainer width="100%" height="100%">
<ChartComponent
data={data}
margin={{ top: 10, right: 30, left: 0, bottom: 0 }}
>
<defs>
{series.map((s) => (
<linearGradient
key={s.key}
id={`gradient-${s.key}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop offset="5%" stopColor={s.color} stopOpacity={0.3} />
<stop offset="95%" stopColor={s.color} stopOpacity={0} />
</linearGradient>
))}
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="hsl(var(--border))"
opacity={0.3}
/>
<XAxis
dataKey="timestamp"
tickFormatter={formatXAxis}
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
interval="preserveStartEnd"
minTickGap={30}
/>
<YAxis
tickFormatter={yAxisFormatter}
stroke="hsl(var(--muted-foreground))"
fontSize={12}
tickLine={false}
axisLine={false}
/>
<Tooltip content={<CustomTooltip />} />
<Legend
wrapperStyle={{ paddingTop: '20px' }}
iconType="circle"
/>
{series.map((s) =>
chartType === 'area' ? (
<Area
key={s.key}
type="monotone"
dataKey={s.key}
name={s.name}
stroke={s.color}
fill={`url(#gradient-${s.key})`}
strokeWidth={2}
dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
/>
) : (
<Line
key={s.key}
type="monotone"
dataKey={s.key}
name={s.name}
stroke={s.color}
strokeWidth={2}
dot={false}
activeDot={{ r: 4, strokeWidth: 0 }}
/>
)
)}
</ChartComponent>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}
// Pre-configured chart for cost metrics
export function CostTimeSeriesChart({
data,
title = 'Cost Over Time',
description = 'Cumulative costs by service',
}: {
data: TimeSeriesDataPoint[];
title?: string;
description?: string;
}) {
const series = [
{ key: 'sqs_cost', name: 'SQS', color: '#FF9900', type: 'area' as const },
{ key: 'lambda_cost', name: 'Lambda', color: '#F97316', type: 'area' as const },
{ key: 'bedrock_cost', name: 'Bedrock', color: '#8B5CF6', type: 'area' as const },
];
return (
<TimeSeriesChart
data={data}
series={series}
title={title}
description={description}
yAxisFormatter={formatCurrency}
chartType="area"
/>
);
}
// Pre-configured chart for request metrics
export function RequestTimeSeriesChart({
data,
title = 'Requests Over Time',
description = 'Request volume trends',
}: {
data: TimeSeriesDataPoint[];
title?: string;
description?: string;
}) {
const series = [
{ key: 'requests', name: 'Requests', color: '#3B82F6', type: 'line' as const },
{ key: 'errors', name: 'Errors', color: '#EF4444', type: 'line' as const },
];
return (
<TimeSeriesChart
data={data}
series={series}
title={title}
description={description}
yAxisFormatter={formatNumber}
chartType="line"
/>
);
}