8.5 KiB
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:
BookieDbContextmaps every table/column explicitly to snake_case names and usesmodelBuilder.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
bookieschema
3. Configure & run the backend
Edit BookieApi/src/Bookie.Api/appsettings.json:
"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 theBookie.Apiproject). The OpenAI key can be supplied the same way (dotnet user-secrets set "OpenAI:ApiKey" "sk-...") or via theOPENAI_API_KEYenvironment variable, which is used automatically whenOpenAI:ApiKeyis blank.
Run:
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
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
finishedmatches only (2 dp). - Form — last 5 finished matches before kickoff, oldest → newest, as
W/D/L. (The Python script usesW/R/P; the API normalizes toW/D/Lper 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=falseand the sub-objects reporthasData=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=Nwidens the window to include the current season plus up to N previous seasons of the same league; defaults to0(current season only). Each match reports theseasonsIncludednames. - 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/Lchip 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
adminrole. - 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/userroles, 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 Entitiesfrom 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: thematchtimestamps aretimestamp without time zone; the API writes them withUnspecifiedkind to satisfy Npgsql.- The API does not manage
matchdayas a full CRUD resource (not requested); it is exposed as a lookup so matches can be created/edited.