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
8 changes: 8 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
node_modules
dist
build
coverage
.gga
openspec
*.min.js
package-lock.json
9 changes: 9 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"singleQuote": false,
"trailingComma": "es5",
"arrowParens": "always",
"endOfLine": "lf"
}
28 changes: 14 additions & 14 deletions frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,20 @@ frontend/

## Páginas

| Ruta | Componente | Descripción |
|------|------------|-------------|
| `/` | Home | Inicio |
| `/historial` | History | Historial de sorteos |
| `/estadisticas` | Statistics | Estadísticas |
| `/heatmaps` | Heatmaps | Mapas de calor |
| `/tendencias` | Trends | Tendencias |
| `/redes` | Networks | Grafos |
| `/monte-carlo` | MonteCarlo | Simulación |
| `/ia` | IA | Asistente |
| `/modelos` | Models | Modelos ML |
| `/experimentos` | Experiments | Experimentos |
| `/backtesting` | Backtesting | Backtesting |
| `/generador` | Generator | Generador |
| Ruta | Componente | Descripción |
| --------------- | ----------- | -------------------- |
| `/` | Home | Inicio |
| `/historial` | History | Historial de sorteos |
| `/estadisticas` | Statistics | Estadísticas |
| `/heatmaps` | Heatmaps | Mapas de calor |
| `/tendencias` | Trends | Tendencias |
| `/redes` | Networks | Grafos |
| `/monte-carlo` | MonteCarlo | Simulación |
| `/ia` | IA | Asistente |
| `/modelos` | Models | Modelos ML |
| `/experimentos` | Experiments | Experimentos |
| `/backtesting` | Backtesting | Backtesting |
| `/generador` | Generator | Generador |

## Tests

Expand Down
23 changes: 7 additions & 16 deletions frontend/e2e/core-cycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,8 @@ async function seedDraws(request: APIRequestContext, lotteryCode: string) {
const dir = mkdtempSync(join(tmpdir(), "lip-e2e-seed-"));
const csvPath = join(dir, "seed.csv");
const header = "draw_number,draw_date,numbers,super_number,jackpot,winners";
const rows = DRAW_ROWS.map(
([drawNumber, drawDate, numbers, jackpot, winners]) =>
[drawNumber, drawDate, `"${numbers.join(",")}"`, "", jackpot, winners].join(","),
const rows = DRAW_ROWS.map(([drawNumber, drawDate, numbers, jackpot, winners]) =>
[drawNumber, drawDate, `"${numbers.join(",")}"`, "", jackpot, winners].join(",")
);
writeFileSync(csvPath, [header, ...rows].join("\n") + "\n", "utf8");
try {
Expand Down Expand Up @@ -94,27 +93,19 @@ test("core cycle: seed → statistics snapshot → dashboard (TEST-005, D3)", as
// --- Statistics: generate snapshot → charts render -----------------------
await page.goto("/estadisticas");
await page.getByRole("button", { name: "Generate Snapshot" }).click();
await expect(
page.getByRole("img", { name: "Frequency distribution per number" }),
).toBeVisible();
await expect(page.getByRole("img", { name: "Frequency distribution per number" })).toBeVisible();
await expect(page.locator(".recharts-responsive-container")).toBeVisible();
await expect(page.getByText(/Snapshot #\d+/)).toBeVisible();

// --- Home: dashboard renders draws + frequencies from the snapshot --------
await page.goto("/");
await expect(page.locator("tbody")).toContainText("1 - 6 - 11 - 16 - 21");
await expect(
page.getByRole("heading", { name: "Frequency summary" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Most frequent" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Least frequent" }),
).toBeVisible();
await expect(page.getByRole("heading", { name: "Frequency summary" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Most frequent" })).toBeVisible();
await expect(page.getByRole("heading", { name: "Least frequent" })).toBeVisible();
// 1 and 6 each appear in two seeded draws → top frequency count is 2×.
await expect(page.locator("ol").first()).toContainText("2×");
// Frontend ↔ backend connectivity is proven by the System health block.
await expect(page.getByText("API status", { exact: true })).toBeVisible();
await expect(page.locator("dl")).toContainText("ok");
});
});
7 changes: 2 additions & 5 deletions frontend/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ export default tseslint.config(
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
},
},
}
);
4 changes: 2 additions & 2 deletions frontend/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default defineConfig({
{
// Migrate a fresh tmp DB, then serve the API from it on :8000.
command:
'DB=$(mktemp /tmp/lip-e2e-db-XXXXXX.db) && ' +
"DB=$(mktemp /tmp/lip-e2e-db-XXXXXX.db) && " +
'LIP_DATABASE_URL="sqlite:///$DB" .venv/bin/python -m alembic upgrade head && ' +
'LIP_DATABASE_URL="sqlite:///$DB" .venv/bin/uvicorn backend.app.main:create_app --factory --host 127.0.0.1 --port 8000',
cwd: BACKEND_DIR,
Expand All @@ -59,4 +59,4 @@ export default defineConfig({
env: { ...process.env, VITE_API_BASE_URL: "http://localhost:8000/api/v1" },
},
],
});
});
54 changes: 25 additions & 29 deletions frontend/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ const env = (data: unknown) =>
HttpResponse.json({ success: true, data, timestamp: "2026-01-01T00:00:00Z" });

const server = setupServer(
http.get("*/api/v1/lotteries", () =>
env([{ id: 1, code: "L1", name: "Loto", country: "ES" }]),
),
http.get("*/api/v1/lotteries", () => env([{ id: 1, code: "L1", name: "Loto", country: "ES" }])),
http.get("*/api/v1/draws", () => env([])),
http.get("*/api/v1/statistics/L1/frequencies", () =>
env({
Expand All @@ -27,11 +25,11 @@ const server = setupServer(
draw_count: 0,
checksum: "x",
frequencies: [],
}),
})
),
http.get("*/api/v1/health", () => env({ status: "ok" })),
http.get("*/api/v1/version", () =>
env({ version: "1.0.0", app: "Lottery Intelligence Platform" }),
env({ version: "1.0.0", app: "Lottery Intelligence Platform" })
),
http.get("*/api/v1/ml/models", () =>
env({
Expand All @@ -43,7 +41,7 @@ const server = setupServer(
checksum: "abc",
input_fingerprint: "fp1",
cut: 1200,
}),
})
),
http.get("*/api/v1/ml/metrics", () => env([])),
http.get("*/api/v1/dl/models", () =>
Expand All @@ -57,7 +55,7 @@ const server = setupServer(
input_fingerprint: "fp-dl",
cut: 305,
window: 10,
}),
})
),
http.get("*/api/v1/dl/metrics", () => env([])),
http.get("*/api/v1/probability/L1/probabilities", () =>
Expand All @@ -71,10 +69,16 @@ const server = setupServer(
draw_count: 100,
checksum: "abc",
probabilities: [
{ model_id: "baseline", model_version: "v1", subject: "5", draw_number: 101, value: "0.1420" },
{
model_id: "baseline",
model_version: "v1",
subject: "5",
draw_number: 101,
value: "0.1420",
},
],
}),
),
})
)
);

function renderAt(path: string) {
Expand Down Expand Up @@ -104,7 +108,7 @@ describe("App router", () => {

expect(container.querySelector("[aria-busy='true']")).not.toBeNull();
expect(
await screen.findByRole("heading", { name: /operational summary/i }, ASYNC_TIMEOUT),
await screen.findByRole("heading", { name: /operational summary/i }, ASYNC_TIMEOUT)
).toBeInTheDocument();
}, 20000);

Expand All @@ -113,14 +117,10 @@ describe("App router", () => {
renderAt("/");

expect(
await screen.findByRole("heading", { name: /operational summary/i }, ASYNC_TIMEOUT),
).toBeInTheDocument();
expect(
screen.getByRole("navigation", { name: "Main navigation" }),
).toBeInTheDocument();
expect(
screen.getByRole("complementary", { name: "Sidebar" }),
await screen.findByRole("heading", { name: /operational summary/i }, ASYNC_TIMEOUT)
).toBeInTheDocument();
expect(screen.getByRole("navigation", { name: "Main navigation" })).toBeInTheDocument();
expect(screen.getByRole("complementary", { name: "Sidebar" })).toBeInTheDocument();
}, 20000);

it("navigates between routes through the sidebar links", async () => {
Expand All @@ -131,15 +131,15 @@ describe("App router", () => {
fireEvent.click(screen.getByRole("link", { name: "Mis Números" }));

expect(
await screen.findByRole("heading", { name: /mis n[uú]meros/i }, ASYNC_TIMEOUT),
await screen.findByRole("heading", { name: /mis n[uú]meros/i }, ASYNC_TIMEOUT)
).toBeInTheDocument();
});

it("renders a 404 fallback for unknown routes", async () => {
renderAt("/unknown");

expect(
await screen.findByRole("heading", { name: /page not found/i }, ASYNC_TIMEOUT),
await screen.findByRole("heading", { name: /page not found/i }, ASYNC_TIMEOUT)
).toBeInTheDocument();
});

Expand All @@ -148,31 +148,27 @@ describe("App router", () => {
renderAt("/ia");

expect(
await screen.findByRole("heading", { name: /ai assistant/i }, ASYNC_TIMEOUT),
).toBeInTheDocument();
expect(
screen.getByRole("region", { name: "System status" }),
await screen.findByRole("heading", { name: /ai assistant/i }, ASYNC_TIMEOUT)
).toBeInTheDocument();
expect(screen.getByRole("region", { name: "System status" })).toBeInTheDocument();
}, 20000);

it("deep-links to the Deep Learning page at /dl through its lazy chunk", async () => {
useLotteryStore.setState({ selectedLotteryId: 1, selectedLotteryCode: "L1" });
renderAt("/dl");

expect(
await screen.findByRole("heading", { name: /deep learning/i }, ASYNC_TIMEOUT),
).toBeInTheDocument();
expect(
screen.getByRole("region", { name: "Deep learning results" }),
await screen.findByRole("heading", { name: /deep learning/i }, ASYNC_TIMEOUT)
).toBeInTheDocument();
expect(screen.getByRole("region", { name: "Deep learning results" })).toBeInTheDocument();
}, 20000);

it("deep-links to the Mis Números page at /numeros through its lazy chunk", async () => {
useLotteryStore.setState({ selectedLotteryId: 1, selectedLotteryCode: "L1" });
renderAt("/numeros");

expect(
await screen.findByRole("heading", { name: /mis números/i }, ASYNC_TIMEOUT),
await screen.findByRole("heading", { name: /mis números/i }, ASYNC_TIMEOUT)
).toBeInTheDocument();
}, 20000);
});
12 changes: 2 additions & 10 deletions frontend/src/charts/AverageChart.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";

export interface AverageSeriesRow {
series_key: string;
Expand Down Expand Up @@ -44,4 +36,4 @@ export default function AverageChart({ series }: AverageChartProps) {
</ResponsiveContainer>
</div>
);
}
}
17 changes: 3 additions & 14 deletions frontend/src/charts/DistributionChart.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";

export interface DistributionRow {
subject: string;
Expand All @@ -29,10 +21,7 @@ function parseValue(value: number | string): number | undefined {
export default function DistributionChart({ rows }: DistributionChartProps) {
const data: { subject: string; value: number }[] = rows
.map((row) => ({ subject: row.subject, value: parseValue(row.value) }))
.filter(
(row): row is { subject: string; value: number } =>
row.value !== undefined,
);
.filter((row): row is { subject: string; value: number } => row.value !== undefined);

return (
<div
Expand All @@ -52,4 +41,4 @@ export default function DistributionChart({ rows }: DistributionChartProps) {
</ResponsiveContainer>
</div>
);
}
}
12 changes: 2 additions & 10 deletions frontend/src/charts/FrequencyChart.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
import {
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import type { FrequencyRow } from "../types/statistics";

export interface FrequencyChartProps {
Expand All @@ -33,4 +25,4 @@ export default function FrequencyChart({ rows }: FrequencyChartProps) {
</ResponsiveContainer>
</div>
);
}
}
2 changes: 1 addition & 1 deletion frontend/src/charts/GapChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,4 @@ export default function GapChart({ rows }: GapChartProps) {
</ResponsiveContainer>
</div>
);
}
}
Loading
Loading