This commit is contained in:
jpmvaz
2026-09-13 20:20:06 +01:00
commit 6994063c71
93 changed files with 7613 additions and 0 deletions
+697
View File
@@ -0,0 +1,697 @@
# Primeira Liga Stats
A self-hosted, real-time football statistics dashboard for the **Portuguese Primeira Liga** (and optionally UEFA Euro). Built with Next.js 15, PostgreSQL 17, and the [API-Football](https://www.api-football.com/) data provider. The entire stack runs as Docker containers — one command to deploy.
---
## Table of Contents
1. [Overview](#overview)
2. [Tech Stack](#tech-stack)
3. [Architecture](#architecture)
4. [Project Structure](#project-structure)
5. [Prerequisites](#prerequisites)
6. [Quick Start](#quick-start)
7. [Configuration](#configuration)
8. [Environment Variables](#environment-variables)
9. [Features](#features)
10. [Pages & Routes](#pages--routes)
11. [API Endpoints](#api-endpoints)
12. [Database](#database)
13. [Admin Panel](#admin-panel)
14. [Performance & Caching](#performance--caching)
15. [Docker Reference](#docker-reference)
16. [Data Persistence & Backups](#data-persistence--backups)
17. [Changing the Port](#changing-the-port)
18. [API Usage & Rate Limits](#api-usage--rate-limits)
19. [Troubleshooting](#troubleshooting)
20. [Development (Local, without Docker)](#development-local-without-docker)
21. [System Requirements](#system-requirements)
---
## Overview
Primeira Liga Stats is a production-ready web dashboard that displays live and historical football data. It polls [API-Football v3](https://www.api-football.com/documentation-v3) for match data and presents it in a clean, auto-refreshing UI. All configuration — including the API key, color theme, visible sections, and refresh interval — can be changed at runtime through the built-in Admin Panel without restarting the application.
---
## Tech Stack
| Layer | Technology | Version |
|---|---|---|
| Framework | Next.js | 15.1.0 |
| Language | TypeScript | 5.7 |
| Runtime | Node.js | 22 LTS (Alpine) |
| Database | PostgreSQL | 17 Alpine |
| ORM | Prisma | 5.22 |
| UI Components | Radix UI | Latest |
| Styling | Tailwind CSS | 3.4 |
| Icons | Lucide React | 0.460 |
| Container Runtime | Docker / Docker Compose | — |
| Data Source | API-Football v3 | — |
---
## Architecture
```
┌─────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌──────────────────┐ ┌─────────────┐ │
│ │ app container │ │ db container│ │
│ │ (Next.js 15) │◄─┤ (Postgres17)│ │
│ │ Node 22 Alpine │ │ │ │
│ │ Port 3000 │ │ Port 5432 │ │
│ └────────┬─────────┘ └─────────────┘ │
│ │ │
└───────────┼─────────────────────────────┘
▼ HTTP
http://localhost:3000
▲ HTTPS
API-Football v3 (external)
v3.football.api-sports.io
```
The Next.js app acts as a **backend-for-frontend**: all API-Football requests are proxied through Next.js API routes. The browser never sees the API key. Settings and request logs are stored in PostgreSQL via Prisma.
---
## Project Structure
```
primeira_liga_stats/
├── Dockerfile # Multi-stage Docker build (Node 22)
├── docker-compose.yml # Orchestrates app + db services
├── .dockerignore # Excludes node_modules, .next, etc.
├── start.sh # Container entrypoint: migrate + start
├── init-db/
│ └── 01-init.sql # Auto-runs on first DB container start
└── nextjs_space/
├── package.json.docker # Dependencies (used by Dockerfile)
├── next.config.js # Next.js config (standalone, compression)
├── tailwind.config.ts # Tailwind theme + dark mode config
├── tsconfig.json # TypeScript compiler config
├── postcss.config.js # PostCSS config
├── components.json # shadcn/ui component registry
├── prisma/
│ └── schema.prisma # Data models: Settings, ApiRequestLog
├── lib/
│ ├── db.ts # Prisma client singleton (with dev logging)
│ ├── settings.ts # Settings fetch + 30s in-memory cache
│ ├── settings-context.tsx # React context for settings
│ ├── api-logger.ts # Non-blocking request logger + pruning
│ ├── types.ts # Shared TypeScript types
│ └── utils.ts # Utility helpers (cn, etc.)
├── app/
│ ├── layout.tsx # Root layout (Inter font, metadata)
│ ├── page.tsx # Main dashboard page (client component)
│ ├── globals.css # Global CSS + Tailwind directives
│ ├── theme-provider.tsx # next-themes provider
│ ├── _components/ # Page-level UI components
│ │ ├── live-matches.tsx # Live score cards with auto-refresh
│ │ ├── standings.tsx # Full league table
│ │ ├── top-scorers.tsx # Top scorers leaderboard
│ │ ├── recent-results.tsx # Last 10 match results
│ │ ├── upcoming-fixtures.tsx # Next 10 fixtures
│ │ └── countdown-timer.tsx # Refresh countdown in header
│ ├── admin/
│ │ ├── layout.tsx # Admin layout wrapper
│ │ ├── page.tsx # Admin dashboard
│ │ └── _components/
│ │ └── api-stats-chart.tsx # Request volume chart
│ └── api/
│ ├── fixtures/route.ts # GET /api/fixtures?type=live|last|next
│ ├── standings/route.ts # GET /api/standings
│ ├── topscorers/route.ts # GET /api/topscorers
│ ├── settings/route.ts # GET /api/settings (public, no API key)
│ └── admin/
│ ├── settings/route.ts # GET|PUT /api/admin/settings
│ └── stats/route.ts # GET /api/admin/stats
├── components/
│ ├── theme-provider.tsx
│ └── ui/ # Radix-based shadcn/ui components
└── public/
├── favicon.svg
└── og-image.png
```
---
## Prerequisites
- **Docker Engine** 20.10 or later
- **Docker Compose** v2.0 or later (`docker compose` not `docker-compose`)
- At least **1 GB RAM** available to Docker
- At least **2 GB free disk space**
- An **API-Football API key** (free tier available at [api-football.com](https://www.api-football.com/))
---
## Quick Start
```bash
# 1. Clone or unzip the project
cd primeira_liga_stats
# 2. Start everything
docker compose up -d
# 3. Open the app (give it ~30 seconds on first run for the build)
open http://localhost:3000
```
The Admin Panel is at `http://localhost:3000/admin`.
That's it. No other setup is needed. The API key is pre-configured in `docker-compose.yml`.
---
## Configuration
All configuration lives in two places:
### 1. `docker-compose.yml` — environment variables (startup time)
Edit this file to change the API key, port, or secrets before starting the containers. Requires a container restart to take effect.
### 2. Admin Panel at `/admin` — runtime settings
Change site name, colors, visible sections, refresh interval, API key, and league without restarting. Settings are persisted to the database and take effect immediately.
---
## Environment Variables
These are set in the `app` service of `docker-compose.yml`:
| Variable | Default | Description |
|---|---|---|
| `DATABASE_URL` | `postgresql://primeiraliga:primeiraliga2024@db:5432/primeiraliga` | Full Postgres connection string. Includes pool settings. |
| `API_FOOTBALL_API_KEY` | *(pre-configured)* | Your API-Football v3 key. Can also be set via Admin Panel. |
| `NEXTAUTH_URL` | `http://localhost:3000` | Public URL of the app. Change this when deploying to a domain. |
| `NEXTAUTH_SECRET` | *(pre-configured)* | Secret used to sign sessions. Change this in production. |
| `NODE_ENV` | `production` | Set automatically. Do not change. |
> **Security note:** Before deploying publicly, generate a new `NEXTAUTH_SECRET` with `openssl rand -base64 32` and update `docker-compose.yml`.
---
## Features
### Main Dashboard (`/`)
- **Live Matches** — real-time scores with elapsed time indicator; auto-refreshes every 30 seconds when there are live games
- **League Standings** — full table with position, team logo, played/won/drawn/lost, goal difference, and points
- **Top Scorers** — ranked list of top goal scorers with player photo, team, and statistics
- **Recent Results** — last 10 completed match results
- **Upcoming Fixtures** — next 10 scheduled matches with date and time
- **Countdown Timer** — header widget showing time until next data refresh
- **Configurable theme** — primary and secondary colors applied across gradients and accents
### Admin Panel (`/admin`)
- Toggle each dashboard section on/off
- Change site name and brand colors
- Switch between **Primeira Liga** (League ID 94, season 2024) and **UEFA Euro 2024** (League ID 4)
- Update the API-Football API key at runtime
- View API request statistics: total requests, today's requests, average response time, success rate, and an hourly request volume chart
---
## Pages & Routes
| Route | Type | Description |
|---|---|---|
| `/` | Client page | Main stats dashboard |
| `/admin` | Client page | Admin configuration panel |
---
## API Endpoints
All routes are internal — they proxy requests to API-Football and return JSON. The browser never sends the API key directly.
### `GET /api/fixtures?type=<live|last|next>`
Fetches fixtures from API-Football.
| `type` param | Data returned | Revalidation |
|---|---|---|
| `live` | Currently live matches | 30 seconds |
| `last` | Last 10 completed results | 5 minutes |
| `next` | Next 10 upcoming fixtures | 5 minutes |
Responses include `Cache-Control: public, s-maxage=N, stale-while-revalidate=2N` headers.
### `GET /api/standings`
Returns the full league standings table. Cached for 5 minutes.
### `GET /api/topscorers`
Returns the top scorers list. Cached for 5 minutes.
### `GET /api/settings`
Returns public settings (no API key). Used by the frontend to load theme, visibility flags, and refresh interval.
### `GET /api/admin/settings`
Returns full settings including the API key. Admin use only.
### `PUT /api/admin/settings`
Updates settings. Accepts a JSON body with any subset of the settings fields. Automatically busts the server-side settings cache.
### `GET /api/admin/stats`
Returns API request analytics: total count, today's count, per-endpoint breakdown, hourly chart data (last 24h), average response time, and success rate. All database queries run in parallel via `Promise.all`.
---
## Database
### Engine
PostgreSQL 17 (Alpine), running in its own container (`primeira_liga_db`).
### Connection
| Property | Value |
|---|---|
| Host (from app container) | `db` |
| Port | `5432` |
| Database | `primeiraliga` |
| User | `primeiraliga` |
| Password | `primeiraliga2024` |
### Schema
Managed by Prisma. There are two tables:
**`Settings`** — singleton row (`id = 'main'`) storing all runtime configuration.
| Column | Type | Default |
|---|---|---|
| `id` | TEXT (PK) | `'main'` |
| `apiKey` | TEXT? | `null` |
| `selectedLeague` | TEXT | `'primeira_liga'` |
| `leagueId` | INT | `94` |
| `season` | INT | `2024` |
| `siteName` | TEXT | `'Primeira Liga Stats'` |
| `primaryColor` | TEXT | `'#E42518'` |
| `secondaryColor` | TEXT | `'#006600'` |
| `showLiveMatches` | BOOL | `true` |
| `showStandings` | BOOL | `true` |
| `showTopScorers` | BOOL | `true` |
| `showRecentResults` | BOOL | `true` |
| `showUpcoming` | BOOL | `true` |
| `refreshInterval` | INT | `300` (seconds) |
| `createdAt` | TIMESTAMP | auto |
| `updatedAt` | TIMESTAMP | auto |
**`ApiRequestLog`** — append-only log of every outbound API-Football call.
| Column | Type | Description |
|---|---|---|
| `id` | TEXT (PK) | cuid() |
| `endpoint` | TEXT | e.g. `/fixtures?type=live` |
| `status` | INT | HTTP status code |
| `duration` | INT | Milliseconds |
| `createdAt` | TIMESTAMP | Indexed for fast range queries |
Logs older than 7 days are automatically pruned in the background (at most once per hour).
### Initialization
On the very first `docker compose up`, PostgreSQL runs `init-db/01-init.sql` which creates both tables and inserts a default `Settings` row. Subsequent starts skip this file. Prisma's `db push` in `start.sh` ensures the schema stays in sync.
### Connecting directly
```bash
docker exec -it primeira_liga_db psql -U primeiraliga -d primeiraliga
```
Useful SQL:
```sql
-- View current settings
SELECT * FROM "Settings";
-- View last 20 API requests
SELECT endpoint, status, duration, "createdAt"
FROM "ApiRequestLog"
ORDER BY "createdAt" DESC
LIMIT 20;
-- Count requests today
SELECT COUNT(*) FROM "ApiRequestLog"
WHERE "createdAt" >= CURRENT_DATE;
```
---
## Admin Panel
Navigate to `http://localhost:3000/admin`.
### Layout Settings tab
- **Site Name** — displayed in the browser tab and page header
- **Primary Color** — used for live indicators, headings, and gradient left side
- **Secondary Color** — used for gradient right side and accent elements
- **Section toggles** — individually enable/disable Live Matches, Standings, Top Scorers, Recent Results, Upcoming Fixtures
- **Refresh Interval** — how often the dashboard auto-refreshes (in seconds; default 300)
### API Configuration tab
- **API Key** — view the currently configured key (masked) or enter a new one. Saved to the database immediately; no restart needed.
- **League Switcher** — toggle between Primeira Liga 2024/25 (League ID 94) and UEFA Euro 2024 (League ID 4)
### Statistics tab
- Total API requests logged
- Requests made today
- Average response time (ms)
- Success rate (%)
- Hourly bar chart of the last 24 hours
- Per-endpoint request breakdown
---
## Performance & Caching
### Server-side settings cache
`lib/settings.ts` caches the Settings row in process memory for 30 seconds. This means API routes don't make a database round-trip on every request. The cache is immediately invalidated whenever settings are updated via `PUT /api/admin/settings`.
### Next.js `fetch` cache
All outbound calls to API-Football use Next.js's extended `fetch` with `next: { revalidate: N }`:
- Live fixtures: revalidate every **30 seconds**
- Standings, top scorers, last/next fixtures: revalidate every **5 minutes**
### HTTP Cache-Control headers
API route responses include `Cache-Control: public, s-maxage=N, stale-while-revalidate=2N`, making them compatible with CDN edge caching if you front the app with Cloudflare or a similar proxy.
### Non-blocking logging
`logApiRequest()` in `lib/api-logger.ts` is fire-and-forget — it returns immediately and never adds latency to the response. DB write failures are caught silently.
### Parallel database queries
The admin stats endpoint runs all 6 database queries simultaneously with `Promise.all`, reducing response time from ~6× (sequential) to ~1× (parallel) query latency.
### Docker build layer caching
`package.json` is copied and `npm ci` is run before any source code is copied, so dependency installation is only re-run when `package.json` changes (not on every code change).
---
## Docker Reference
### Start
```bash
docker compose up -d
```
### Stop (keep data)
```bash
docker compose down
```
### Stop and delete all data
```bash
docker compose down -v
```
### View all logs (live)
```bash
docker compose logs -f
```
### View app logs only
```bash
docker compose logs -f app
```
### View database logs only
```bash
docker compose logs -f db
```
### Rebuild after code changes
```bash
docker compose up -d --build
```
### Restart only the app (not the DB)
```bash
docker compose restart app
```
### Check container status
```bash
docker compose ps
```
### Container names
| Container | Service |
|---|---|
| `primeira_liga_app` | Next.js application |
| `primeira_liga_db` | PostgreSQL 17 |
---
## Data Persistence & Backups
Database data lives in the `postgres_data` Docker volume and persists across `docker compose down` (without `-v`).
### Backup
```bash
docker exec primeira_liga_db \
pg_dump -U primeiraliga primeiraliga > backup_$(date +%Y%m%d).sql
```
### Restore
```bash
docker exec -i primeira_liga_db \
psql -U primeiraliga primeiraliga < backup_20260101.sql
```
### Full reset (wipe and start fresh)
```bash
docker compose down -v
docker compose up -d
```
---
## Changing the Port
Edit `docker-compose.yml` under the `app` service:
```yaml
ports:
- "8080:3000" # exposes on host port 8080 instead of 3000
```
Also update `NEXTAUTH_URL` to match:
```yaml
environment:
- NEXTAUTH_URL=http://localhost:8080
```
Then restart:
```bash
docker compose up -d
```
---
## API Usage & Rate Limits
The app uses [API-Football v3](https://www.api-football.com/documentation-v3).
### Free tier limits
- **100 requests per day**
- Data updates available every ~15 minutes on the provider side
### Default request rate in this app
With the default 5-minute refresh interval and 4 data sections (standings, top scorers, live, last/next fixtures), the app makes roughly **45 requests per refresh cycle** = ~5872 requests per 24 hours. This fits within the free tier.
### Conserving requests
- Increase the refresh interval in the Admin Panel (e.g. 600 seconds = 10 minutes)
- Disable sections you don't need (e.g. Top Scorers, Upcoming Fixtures)
- The server-side Next.js `fetch` cache means multiple browser clients hitting the same route share one upstream request
### Monitoring usage
The Admin Panel Statistics tab shows how many requests have been made today and in total.
---
## Troubleshooting
### App won't start
```bash
docker compose logs -f app
```
Common causes: Prisma migration failed (DB not ready yet — `start.sh` waits 5 seconds and the healthcheck retries up to 10 times), or a port conflict.
### Database connection error
```bash
docker compose logs -f db
```
Wait 1015 seconds for the DB to initialise on first run. The app container will retry. If it keeps failing, run `docker compose down -v && docker compose up -d` to start fresh.
### Port 3000 already in use
```bash
# Find what's using it
lsof -i :3000
# Or just change the port in docker-compose.yml
```
### API data not loading / blank sections
1. Verify your API key is valid at [api-football.com](https://www.api-football.com/)
2. Check your daily request limit in the API-Football dashboard
3. Check app logs: `docker compose logs -f app`
4. Open browser DevTools → Network tab and look for failed requests to `/api/*`
### Changes in `docker-compose.yml` not taking effect
```bash
docker compose up -d --force-recreate
```
### Code changes not reflected
```bash
docker compose up -d --build
```
### Prisma client out of sync after schema change
The `start.sh` script runs `prisma db push --skip-generate` on every container start, so schema changes are automatically applied. If you update `schema.prisma`, rebuild the image.
---
## Development (Local, without Docker)
You can run the Next.js app locally for development while pointing at a local or remote Postgres instance.
### 1. Install dependencies
```bash
cd nextjs_space
# Copy the Docker package.json as the local one
cp package.json.docker package.json
npm install
```
### 2. Set up environment
Create `nextjs_space/.env.local`:
```env
DATABASE_URL="postgresql://primeiraliga:primeiraliga2024@localhost:5432/primeiraliga"
API_FOOTBALL_API_KEY="your_key_here"
NEXTAUTH_URL="http://localhost:3000"
NEXTAUTH_SECRET="dev-secret-change-in-production"
```
### 3. Start a local Postgres (optional — if not using Docker DB)
```bash
docker run -d \
--name pg-dev \
-e POSTGRES_USER=primeiraliga \
-e POSTGRES_PASSWORD=primeiraliga2024 \
-e POSTGRES_DB=primeiraliga \
-p 5432:5432 \
postgres:17-alpine
```
### 4. Push the schema
```bash
cd nextjs_space
npx prisma db push
```
### 5. Start the dev server
```bash
npm run dev
```
App available at `http://localhost:3000`.
### Available scripts
| Script | Description |
|---|---|
| `npm run dev` | Start Next.js dev server with hot reload |
| `npm run build` | Generates Prisma client and builds for production |
| `npm run start` | Start the production build |
| `npm run lint` | Run ESLint |
| `npm run db:push` | Sync Prisma schema to DB (no migration files) |
| `npm run db:migrate` | Apply pending Prisma migrations |
---
## System Requirements
| Requirement | Minimum | Recommended |
|---|---|---|
| Docker Engine | 20.10 | Latest stable |
| Docker Compose | v2.0 | Latest stable |
| RAM (for Docker) | 1 GB | 2 GB |
| Disk space | 2 GB | 4 GB |
| CPU | 1 core | 2 cores |
| OS | Linux, macOS, Windows (WSL2) | Linux |
---
## External Links
- [API-Football Documentation](https://www.api-football.com/documentation-v3)
- [API-Football Dashboard (check usage)](https://dashboard.api-football.com/)
- [Next.js 15 Docs](https://nextjs.org/docs)
- [Prisma Docs](https://www.prisma.io/docs)
- [Docker Docs](https://docs.docker.com/)
- [Tailwind CSS Docs](https://tailwindcss.com/docs)