Files
Bookie/README.md
2026-09-17 21:45:57 +02:00

162 lines
8.5 KiB
Markdown

# Bookie — Football Stats Analyzer
A full-stack application over the PostgreSQL **`bookie`** schema:
- **`BookieApi/`** — ASP.NET Core (.NET 10) Web API, layered architecture, EF Core (Npgsql, database-first), JWT auth, Swagger.
- **`BookieClient/`** — Angular 22 (standalone components, signals) + Angular Material, Chart.js, jsPDF.
The headline feature is the **Pre-Match Report**, a faithful port + extension of the original `report.py` analyzer exposed as a service, an API endpoint, and a rich Angular page.
---
## 1. Architecture
### Backend (`BookieApi`) — layered
```
src/
├─ Bookie.Domain # POCO entities mirroring the bookie schema
├─ Bookie.Application # DTOs, service interfaces (no persistence deps)
├─ Bookie.Infrastructure # EF Core DbContext, EF configs, service impls, JWT
└─ Bookie.Api # Controllers, Program.cs, Swagger, JWT wiring
```
- **DbContext**: `BookieDbContext` maps every table/column explicitly to snake_case names and uses `modelBuilder.HasDefaultSchema("bookie")`.
- **Repository/Service pattern**: each entity has a service (`ILeagueService`, `IMatchService`, …) implemented in Infrastructure; the API depends only on the interfaces.
- **DTOs**: read/create/update DTOs with DataAnnotations validation; server-side paging/sorting/filtering via `PagedQuery` / `PagedResult<T>`.
### Frontend (`BookieClient`)
```
src/app/
├─ core/ # models, AuthService (signals), HTTP interceptor, guards, ApiService
├─ layout/ # ShellComponent (toolbar + sidenav)
├─ shared/ # confirm dialog
└─ features/
├─ auth/ # login page
├─ dashboard/ # summary widgets + Chart.js status doughnut
├─ pre-match/ # PRIORITY: Pre-Match Reports page (+ CSV/PDF export)
└─ crud/ # metadata-driven CRUD page + form dialog (all 6 entities)
```
---
## 2. Prerequisites
- .NET SDK 10.x
- Node.js 20+ and npm
- Access to the PostgreSQL database that contains the `bookie` schema
---
## 3. Configure & run the backend
Edit `BookieApi/src/Bookie.Api/appsettings.json`:
```jsonc
"ConnectionStrings": {
// Same database the report.py script used (DB "modwad", schema "bookie").
"BookieDb": "Host=YOUR_HOST;Port=5432;Database=modwad;Username=YOUR_USER;Password=YOUR_PASSWORD;Search Path=bookie"
},
"Jwt": {
"Secret": "replace-with-a-long-random-secret-at-least-32-chars"
},
"OpenAI": {
// Required only for the AI Match Prediction feature. Model must support structured outputs.
"ApiKey": "sk-...your-key...",
"Model": "gpt-5.6-luna"
}
```
> **Tip:** keep secrets out of source control with user-secrets:
> `dotnet user-secrets set "ConnectionStrings:BookieDb" "..."` (run in the `Bookie.Api` project).
> The OpenAI key can be supplied the same way (`dotnet user-secrets set "OpenAI:ApiKey" "sk-..."`)
> or via the `OPENAI_API_KEY` environment variable, which is used automatically when `OpenAI:ApiKey` is blank.
Run:
```bash
cd BookieApi
dotnet restore
dotnet run --project src/Bookie.Api --urls http://localhost:5210
```
- Swagger UI: <http://localhost:5210/swagger>
- The API uses the existing schema as-is (database-first). It does **not** create or migrate tables.
### Demo accounts (in-memory, configured in `appsettings.json` → `AuthUsers`)
| Username | Password | Role |
|----------|------------|-------|
| `admin` | `admin123` | admin |
| `user` | `user123` | user |
`admin` can create/update/delete; `user` has read-only access. (The `bookie` schema has no users table, so auth is a simple configurable in-memory list.)
---
## 4. Run the frontend
```bash
cd BookieClient
npm install
npm start # ng serve on http://localhost:4200
```
The client points to `http://localhost:5210/api` (see `src/environments/environment.ts`). CORS on the API allows `http://localhost:4200` by default (configurable via `Cors:AllowedOrigins`).
---
## 5. The Pre-Match Report feature
**Endpoint:** `GET /api/reports/pre-match?date=YYYY-MM-DD` (defaults to today).
Returns all matches on the date (any status) grouped by **league / country / season**, ordered by country, league name, then kickoff. For **each match**, and for **both teams**, it computes stats using **only matches strictly before that match's `kickoff_at`, within the same season, excluding the match itself** — mirroring the Python `exclude_match_id` + `kickoff_at < before_date` filters exactly:
- **Season averages** (from `match_team_stats`): possession, shots, shots on target, corners, fouls, offsides (1 dp), yellow/red cards (2 dp).
- **Goals for / against per match** — from `finished` matches only (2 dp).
- **Form** — last 5 finished matches before kickoff, oldest → newest, as `W` / `D` / `L`. (The Python script uses `W`/`R`/`P`; the API normalizes to `W`/`D`/`L` per the DTO contract.)
- **Top 5 scorers** — goals + assists for the team, counted from matches before this one.
- **Graceful empty state**: when a team has no prior matches, `hasHistory=false` and the sub-objects report `hasData=false` (no errors).
**Efficiency:** rather than 4 queries per team per match (N+1), the service loads all season-scoped matches/stats/goals that could be relevant in a handful of set-based EF Core queries, then applies the exact per-match `before`/`exclude` filter in memory.
**Angular page** (`/pre-match`): date picker (default today), collapsible league/season sections, per-match cards (kickoff, teams, status badge, score, stadium/referee), two side-by-side team panels (goals, season averages grid, form as colored chips, top-5 scorers table), loading skeletons, empty/error states, and **CSV + PDF export**.
**Extensions:**
- **History depth selector** — `?seasonsBack=N` widens the window to include the current season plus up to _N_ previous seasons of the same league; defaults to `0` (current season only). Each match reports the `seasonsIncluded` names.
- **Head-to-head** — `GET /api/reports/head-to-head?teamAId=&teamBId=&beforeMatchId=` returns previous finished meetings between the two teams (with a W/D/W summary and goal aggregate). A **Head-to-head** button on each card opens a dialog; each meeting drills into full match stats.
- **Clickable form chips** — hovering a `W/D/L` chip shows the fixture and score; clicking opens that match's full stats.
---
## 6. AI Match Prediction
**Endpoint:** `POST /api/predictions/match/{matchId}`.
The `PredictionService` gathers structured historical data for the fixture — season overall averages, **home/away venue splits**, last-5 recent form with underlying stats, and cross-season **head-to-head** — then sends it to an OpenAI chat model with a dedicated "Football Match Statistics Predictor" system prompt. It uses **structured outputs (JSON schema)** so the response is a typed projection: per-team expected **goals, shots on target, corners, fouls, yellow and red cards** (each a point estimate plus low/high range), a reasoning paragraph, a High/Medium/Low confidence label, and data-quality flags.
Configure `OpenAI:ApiKey` (or the `OPENAI_API_KEY` env var) and optionally `OpenAI:Model` (default `gpt-5.6-luna`). In the UI, a **Predict** button on each match card opens the projection dialog.
> These are statistical estimates for analytical purposes only — not guaranteed outcomes, and the feature offers no betting guidance.
---
## 7. Standard features
- **CRUD** (list with paging/sorting/filtering + create/edit/delete dialogs) for: **leagues, seasons, teams, players, matches, player contracts**. Writes require the `admin` role.
- **Dashboard**: counts of leagues/seasons/teams/players/matches, upcoming matches in the next 7 days, a matches-by-status doughnut chart, and next fixtures.
- **JWT auth** with `admin` / `user` roles, enforced on the API and reflected in the UI (action buttons only for admins).
---
## 8. Assumptions
- **Database-first without live scaffolding**: entities were written by hand to exactly match `schema_bookie.sql` (no DB connection was available at build time). To re-scaffold instead, run:
`dotnet ef dbcontext scaffold "<conn>" Npgsql.EntityFrameworkCore.PostgreSQL --schema bookie --output-dir Entities` from the Infrastructure project.
- **Target framework**: .NET 10 (the current LTS as of this build).
- **Auth users** live in configuration because the schema has no users/roles tables.
- **`updated_at` / timestamps**: the `match` timestamps are `timestamp without time zone`; the API writes them with `Unspecified` kind to satisfy Npgsql.
- The API does not manage `matchday` as a full CRUD resource (not requested); it is exposed as a lookup so matches can be created/edited.