396 lines
15 KiB
Markdown
396 lines
15 KiB
Markdown
# AGENTS.md - Development Guidelines
|
|
|
|
Essential information for AI agents and developers working on this codebase.
|
|
|
|
## Project Overview
|
|
|
|
Web application for "Harmonie de Montpellier-Jacou" (music band) to manage and share sheet music (partitions).
|
|
|
|
- **Frontend**: SvelteKit (Svelte 5, runes) + Tailwind CSS in `partitions/`
|
|
- **Backend**: PHP 8.5 REST API in `api/` (no framework)
|
|
- **Storage**: flat files under `legacy/Scores/` (no database)
|
|
- **Auth**: JWT (HS256), see `api/lib/Auth.php`
|
|
- **Production**: `https://partitions.c.nadal-fr.com` (front) + `https://ohmj-api.c.nadal-fr.com` (API)
|
|
|
|
There is **no MySQL** and **no Vue.js** in the active codebase. The old Vue.js 2 / MySQL
|
|
site is archived in `legacy/` and must not be modified.
|
|
|
|
## Stack & Versions
|
|
|
|
- Node.js 26 / npm 11
|
|
- Svelte 5, SvelteKit 2, `@sveltejs/adapter-static`, Vite 5, Tailwind 3
|
|
- PHP 8.5 (CLI built-in server in development)
|
|
|
|
## Dependencies
|
|
|
|
### Frontend (`partitions/package.json`)
|
|
|
|
Runtime:
|
|
|
|
| Package | Version |
|
|
|---|---|
|
|
| `axios` | ^1.6.0 |
|
|
| `pdfjs-dist` | ^4.0.379 |
|
|
|
|
Dev:
|
|
|
|
| Package | Version |
|
|
|---|---|
|
|
| `svelte` | ^5.0.0 |
|
|
| `@sveltejs/kit` | ^2.0.0 |
|
|
| `@sveltejs/adapter-static` | ^3.0.0 |
|
|
| `@sveltejs/vite-plugin-svelte` | ^4.0.0 |
|
|
| `vite` | ^5.0.0 |
|
|
| `typescript` | ^5.0.0 |
|
|
| `svelte-check` | ^4.0.0 |
|
|
| `tailwindcss` | ^3.4.0 |
|
|
| `autoprefixer` | ^10.4.16 |
|
|
| `postcss` | ^8.4.32 |
|
|
|
|
Install: `cd partitions && npm install`.
|
|
|
|
### Backend (`api/composer.json`)
|
|
|
|
| Package | Version |
|
|
|---|---|
|
|
| `setasign/fpdi` | ^2.6 (import de pages PDF) |
|
|
| `setasign/fpdf` | ^1.8 (base PDF) |
|
|
| `tecnickcom/tcpdf` | ^6.10 |
|
|
|
|
Install: `cd api && php composer.phar install` (le dossier `vendor/` est déjà présent).
|
|
|
|
PHP extensions requises (via `composer.lock`) : `gd`, `zlib`, `curl`. Déjà fournies
|
|
ou activées avec les paquets Arch `php` + `php-gd`.
|
|
|
|
### Tooling de développement
|
|
|
|
- `xdebug` (paquet Arch `extra/xdebug`) — optionnel, utilisé pour mesurer la
|
|
couverture de code du backend (`xdebug.mode=coverage`).
|
|
|
|
> PHP 8.5 note: nullable parameters MUST use explicit `?type` syntax
|
|
> (e.g. `?string $x = null`). Implicit nullable is deprecated and pollutes API output.
|
|
|
|
## Build & Run Commands
|
|
|
|
### Backend (PHP)
|
|
|
|
```bash
|
|
cd api
|
|
php -c php-upload.ini -S localhost:8000 router.php
|
|
```
|
|
|
|
- `-c php-upload.ini` is **required** so PDF uploads work (`upload_max_filesize`/`post_max_size` = 64M).
|
|
`ini_set()` cannot change those directives at runtime.
|
|
- API base URL: `http://localhost:8000` (paths have **no** `/api` prefix in the client).
|
|
- Copy `api/.env.example` to `api/.env` and set a strong `JWT_SECRET`.
|
|
|
|
### Frontend (SvelteKit)
|
|
|
|
```bash
|
|
cd partitions
|
|
npm install
|
|
npm run dev # http://localhost:5173
|
|
npm run build # outputs to partitions/build
|
|
npm run check # svelte-check (0 errors / 0 warnings expected)
|
|
npm run preview
|
|
```
|
|
|
|
- `frontend` is a symlink to `partitions/build` (the deployed static site).
|
|
- `VITE_API_URL` (build/dev env) selects the API base URL; falls back to
|
|
`localhost:8000` (dev) or the production URL.
|
|
- No ESLint / test framework is configured for the frontend.
|
|
|
|
### Deploy
|
|
|
|
```bash
|
|
./deploy/deploy.sh
|
|
```
|
|
|
|
- Do NOT include `legacy/` or `api/tests.php` in the deploy archive.
|
|
- `api/.env` (with `JWT_SECRET`) is part of the deployment.
|
|
- The frontend build must be present in `frontend/`.
|
|
- Production: front `https://partitions.c.nadal-fr.com`, API `https://ohmj-api.c.nadal-fr.com`.
|
|
- **Cache**: hashed assets under `_app/immutable/` are immutable (1y); the SPA
|
|
shell `index.html` MUST be served with `Cache-Control: no-cache` (see
|
|
`partitions/nginx.conf`). Otherwise clients keep a stale shell referencing
|
|
hashed chunks removed by the new deploy, causing errors on some routes (e.g.
|
|
the PDF viewer). The front is served by the `nginx` container (Caddy only
|
|
`reverse_proxy`s to it and passes the headers through), so this is configured
|
|
in `nginx.conf`, not in the Caddyfile. After a deploy, a hard refresh can be
|
|
needed for already-cached clients.
|
|
|
|
### Testing the deploy archive (recommended)
|
|
|
|
It is worth testing the produced zip, not just the source tree. `deploy.sh`
|
|
generates `_builds/deploy_ohmj.zip`; verify it before uploading:
|
|
|
|
```bash
|
|
# 1. Inspect the archive (no stale/test/residue files)
|
|
unzip -l _builds/deploy_ohmj.zip | grep -E 'tests\.php|coverage-|composer\.phar|node_modules|legacy/'
|
|
# -> must print nothing
|
|
unzip -p _builds/deploy_ohmj.zip api/.env # JWT_SECRET + SCORES_PATH
|
|
# 2. Extract and run the artifact end-to-end against the E2E smoke test
|
|
rm -rf /tmp/deploy_test && mkdir -p /tmp/deploy_test
|
|
unzip -q _builds/deploy_ohmj.zip -d /tmp/deploy_test
|
|
# - API: cd /tmp/deploy_test/api
|
|
# SCORES_PATH=<repo>/legacy/Scores/ php -c php-upload.ini -S localhost:8000 router.php
|
|
# - front: serve /tmp/deploy_test/frontend with an SPA fallback (see below)
|
|
API_URL=http://localhost:8000 FRONT_URL=http://localhost:4173 npm run test:e2e --prefix partitions
|
|
# 3. Security: .env / config/users.json must NOT be served
|
|
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/.env # -> 401
|
|
```
|
|
|
|
Notes:
|
|
- `deploy.sh` deletes the previous zip before zipping: `zip -r` alone would
|
|
**merge** into an existing archive and keep stale files from earlier deploys.
|
|
- The built frontend bakes `VITE_API_URL` **and** the CSP `connect-src`
|
|
(`svelte.config.js` only allows `localhost:8000` + the prod API). To test a zip
|
|
locally, either reuse `localhost:8000` or rebuild the front with a permitted URL
|
|
(`VITE_API_URL=http://localhost:8000 npm run build`).
|
|
- The E2E smoke test accepts `API_URL` / `FRONT_URL` so it can target the
|
|
extracted artifact instead of the dev servers.
|
|
|
|
## Testing
|
|
|
|
### Run everything (single command)
|
|
|
|
```bash
|
|
./test.sh
|
|
```
|
|
|
|
Runs, in order: backend tests (`api/tests.php`), backend code coverage (Xdebug,
|
|
if available), frontend `npm run check` (svelte-check), and the frontend E2E smoke
|
|
test. It starts the API and dev servers if needed and stops them afterwards.
|
|
"Test the project" means running `./test.sh`.
|
|
|
|
### PHP API tests
|
|
|
|
```bash
|
|
cd api
|
|
php -c php-upload.ini -S localhost:8000 router.php & # if not already running
|
|
rm -f /tmp/rate_* 2>/dev/null # clear rate limiting first
|
|
php tests.php
|
|
```
|
|
|
|
- Expected: **185/185 tests passed (100%)**.
|
|
- Covers: auth (login, bad credentials, missing/invalid token), scores CRUD,
|
|
create-score-with-pieces, files tree, path traversal, rate limiting, upload
|
|
validation, INI injection, security headers.
|
|
- `tests.php` is kept for development only and MUST NOT be shipped in the deploy zip.
|
|
|
|
### Code coverage (optional, requires Xdebug)
|
|
|
|
The test suite covers ~96% of `api/` non-vendor code (`Auth.php` and `router.php`
|
|
at 100%, `ScoreScanner.php` +99%). The remaining uncovered lines are defensive
|
|
I/O-failure branches (rate-limiter `flock`/`fopen` failures, missing
|
|
`SCORES_PATH`, `OOM`/partial PHP upload errors, `createScore` rollback) that
|
|
need OS-level fault injection to trigger.
|
|
|
|
```bash
|
|
cd api
|
|
rm -rf /tmp/ohmj_cov
|
|
# Terminal 1: instrumented dev server
|
|
php -d zend_extension=xdebug -d xdebug.mode=coverage \
|
|
-d auto_prepend_file="$PWD/coverage-prepend.php" \
|
|
-c php-upload.ini -S localhost:8000 router.php
|
|
# Terminal 2: run tests through the instrumented server
|
|
php -d zend_extension=xdebug -d xdebug.mode=coverage \
|
|
-d auto_prepend_file="$PWD/coverage-prepend.php" tests.php
|
|
# Then, from the repo root:
|
|
SHOW_MISSING=1 php api/coverage-report.php \
|
|
api/index.php api/router.php api/lib/Auth.php api/lib/ScoreScanner.php
|
|
```
|
|
|
|
`coverage-prepend.php` / `coverage-report.php` are dev-only and excluded from the
|
|
deploy archive.
|
|
|
|
### Frontend E2E smoke test (CDP, no dependency)
|
|
|
|
Requires the API (`:8000`) and the frontend dev server (`:5173`) to be running.
|
|
|
|
```bash
|
|
cd partitions
|
|
npm run test:e2e
|
|
```
|
|
|
|
`tests/e2e-smoke.mjs` drives the system `chromium` through the DevTools Protocol:
|
|
it logs in via the API, injects the session, then visits `/scores`, a score
|
|
detail, the PDF viewer and `/admin`. It asserts that every API call returns
|
|
`< 400`, that the SPA stays on each route, and that the PDF is fetched with an
|
|
`Authorization` header (no token in the URL).
|
|
|
|
Env overrides: `API_URL`, `FRONT_URL`, `CHROME_PATH`, `LOGIN_USER`, `LOGIN_PASS`.
|
|
|
|
## Authentication
|
|
|
|
- Login: `POST /login` with `{"username":"admin","password":"password"}` (development).
|
|
- Token: JWT HS256 signed with `JWT_SECRET`, 1 hour expiry.
|
|
- API calls: send `Authorization: Bearer <token>`.
|
|
- Token is stored in `localStorage` on the frontend and attached via an axios request interceptor.
|
|
- Roles: `admin` (full access) and `user` (read-only). Admin routes live under `/admin/...`.
|
|
- Frontend dev login page: http://localhost:5173
|
|
|
|
## API Endpoints
|
|
|
|
Public / all authenticated users:
|
|
|
|
- `POST /login`
|
|
- `GET /scores`
|
|
- `GET /scores/:id`
|
|
- `GET /scores/:id/instruments[?piece=N]`
|
|
- `GET /pieces/:scoreId`
|
|
- `GET /download/:path` (token via `Authorization` header or `?token=`)
|
|
|
|
Admin only (`role === 'admin'`):
|
|
|
|
- `POST /admin/scores`
|
|
- `PUT /admin/scores/:id`
|
|
- `PUT /admin/scores/:id/pieces`
|
|
- `DELETE /admin/scores/:id`
|
|
- `POST /admin/scores/:id/upload`
|
|
- `GET /admin/scores/:id/files`
|
|
- `DELETE /admin/scores/:id/files?path=...`
|
|
|
|
## File Organization
|
|
|
|
```
|
|
/
|
|
├── api/ # PHP REST API (active)
|
|
│ ├── index.php # Router / entry point
|
|
│ ├── router.php # Dev server router (+ CORS, .env loading)
|
|
│ ├── lib/Auth.php # JWT auth
|
|
│ ├── lib/ScoreScanner.php# File-based score index
|
|
│ ├── config/users.json # Users (bcrypt hashes)
|
|
│ ├── tests.php # Functional tests (dev only)
|
|
│ └── .env # JWT_SECRET, SCORES_PATH
|
|
├── partitions/ # SvelteKit app (active)
|
|
│ ├── src/lib/api.ts # Axios client + apiService
|
|
│ ├── src/lib/stores/ # auth store
|
|
│ ├── src/lib/components/ # PdfViewer
|
|
│ ├── src/routes/ # pages (login, scores, admin)
|
|
│ └── build/ # static output (gitignored)
|
|
├── frontend -> partitions/build # symlink used for deployment
|
|
├── deploy/deploy.sh # deploy archive builder
|
|
├── scripts/convert_final_v2.js # one-off filename converter
|
|
├── legacy/ # archived old site + Scores/ (do not modify)
|
|
└── _builds/ # temporary work files (gitignored)
|
|
```
|
|
|
|
## Data Model (files)
|
|
|
|
> Full filename/directory convention: see [`NAMING.md`](NAMING.md). It is
|
|
> normalized and MUST NOT be changed casually (backend + frontend depend on it).
|
|
|
|
```
|
|
legacy/Scores/<NUM>/<PIECE>/<INSTRUMENT>/<VERSION>/<PARTIE>.pdf
|
|
legacy/Scores/<NUM>/score.ini
|
|
```
|
|
|
|
`score.ini`:
|
|
|
|
```ini
|
|
[info]
|
|
name = Nom du morceau
|
|
compositor = Compositeur
|
|
ressource = https://... # optional external link
|
|
|
|
[pieces]
|
|
count = 1
|
|
```
|
|
|
|
- `NUM` = score id (zero-padded, e.g. `102`).
|
|
- `PIECE` = movement/piece number (1 for single-piece scores).
|
|
- `INSTRUMENT` = trigram code (see below).
|
|
- `VERSION` = version number.
|
|
- `PARTIE` = part number.
|
|
|
|
Instrument codes: `dir`, `pic`, `flu`, `cla`, `clb`, `sax`, `sat`, `sab`, `coa`,
|
|
`cba`, `cor`, `trp`, `trb`, `tub`, `htb`, `bas`, `per`, `crn`, `eup`, `har`, `pia`,
|
|
plus `sup`/`par` for combined parts.
|
|
|
|
Admin uploads build the filename from the instrument's **ASCII** name
|
|
(`instruments.ts` → `api/lib/ScoreScanner.php`), with `-` between fields
|
|
(e.g. `clarinette-sib-1.pdf`). For `sup`/`par`, the **"Parties combinées"** editor
|
|
builds the real filename base from one or more instruments (with key/clef/part),
|
|
joined by `+` (e.g. `contrebasse-sib-clefa-1+baryton-sib-clefa-1`). See `NAMING.md`.
|
|
|
|
## Code Style
|
|
|
|
### PHP
|
|
- `<?php` opening tags, PascalCase classes, camelCase methods.
|
|
- 3-space indentation, opening brace on the same line (legacy style).
|
|
- Consistent with surrounding code; prefer `include_once`/`require_once` for includes.
|
|
- No database layer: file operations via `ScoreScanner.php`.
|
|
|
|
### JavaScript / Svelte
|
|
- Svelte 5 runes (`$state`, `$props`, `$derived`, `$effect`); prefer `$app/state` over the deprecated `$app/stores`.
|
|
- TypeScript, camelCase variables, PascalCase components, 2-space indent, semicolons.
|
|
|
|
### General
|
|
- UTF-8, French for user-facing content.
|
|
- Do not add comments unless necessary.
|
|
- **NEVER delete files or directories without asking first.**
|
|
|
|
## Design Guidelines
|
|
|
|
- Clean, sober, modern.
|
|
- Use SVG icons instead of emojis.
|
|
- Integrate related actions (view + download) in a single row with consistent styling.
|
|
- Hide redundant labels ("Piece 1", "Version 1") when there is only one.
|
|
- Use spacing, subtle borders, and hover effects for visual hierarchy.
|
|
|
|
## Security Checklist (before deployment)
|
|
|
|
- [ ] `JWT_SECRET` is strong and unique (`openssl rand -base64 32`); never commit `api/.env`.
|
|
- [ ] `api/config/users.json` uses non-default credentials (change `admin`/`user`).
|
|
- [ ] CORS origins restricted to the real domains (no wildcard fallback).
|
|
- [ ] HTTPS enforced in production.
|
|
- [ ] Rate limiting active (clear `/tmp/rate_*` only in tests, never in prod).
|
|
- [ ] `php tests.php` passes 185/185.
|
|
- [ ] `npm audit` reviewed.
|
|
- [ ] No secrets in code or git history.
|
|
|
|
Manual checks:
|
|
|
|
```bash
|
|
# JWT login
|
|
curl -s http://localhost:8000/login -X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"admin","password":"password"}'
|
|
|
|
# Directory traversal must NOT return 200
|
|
curl -s http://localhost:8000/download/../../../etc/passwd \
|
|
-H "Authorization: Bearer <token>"
|
|
|
|
# Security headers on any response
|
|
curl -s -I http://localhost:8000/login -X POST \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"admin","password":"password"}'
|
|
```
|
|
|
|
> Security fixes applied recently: `api/router.php` no longer serves files
|
|
> statically (`.env`/`config/users.json` are not reachable); `deleteScoreFile`
|
|
> enforces `realpath()` containment and only deletes `.pdf`; `api/.env` and
|
|
> `partitions/.env` are untracked/gitignored (rotate the secret since it remains
|
|
> in git history); JWT uses base64url, requires `exp` and validates `alg`;
|
|
> credentials errors are generic; the `ressource` link rejects non-http(s) URLs.
|
|
>
|
|
> Known findings still open: default `admin`/`password` credentials; login rate
|
|
> limiter is race-prone (no `flock`) and `/api/login` bypasses it; CORS falls
|
|
> back to `*` for unknown origins; `GET /admin/scores/:id` passes extra
|
|
> arguments dropped by `ScoreScanner::updateScore` (so `ressource` is not
|
|
> persisted); watermarking only keeps page 1 of multi-page PDFs; `createScore`
|
|
> and `uploadPdf` do not validate path segments; `updateScore`/`updatePieces`
|
|
> do not sanitize INI values.
|
|
|
|
## Temporary Files
|
|
|
|
- Put temporary scripts and outputs in `_builds/` (gitignored).
|
|
- `scripts/convert_final_v2.js` is the only committed script in `scripts/`.
|
|
|
|
## Git
|
|
|
|
- Default branch: `master`.
|
|
- No CI/CD configured.
|
|
- Only commit when explicitly requested.
|