# 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=` 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 **4–5 requests per refresh cycle** = ~58–72 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 10–15 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)