162 lines
8.5 KiB
Markdown
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.
|