12 KiB
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 Archextra/xdebug) — optionnel, utilisé pour mesurer la couverture de code du backend (xdebug.mode=coverage).
PHP 8.5 note: nullable parameters MUST use explicit
?typesyntax (e.g.?string $x = null). Implicit nullable is deprecated and pollutes API output.
Build & Run Commands
Backend (PHP)
cd api
php -c php-upload.ini -S localhost:8000 router.php
-c php-upload.iniis 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/apiprefix in the client). - Copy
api/.env.exampletoapi/.envand set a strongJWT_SECRET.
Frontend (SvelteKit)
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
frontendis a symlink topartitions/build(the deployed static site).VITE_API_URL(build/dev env) selects the API base URL; falls back tolocalhost:8000(dev) or the production URL.- No ESLint / test framework is configured for the frontend.
Deploy
./deploy/deploy.sh
- Do NOT include
legacy/orapi/tests.phpin the deploy archive. api/.env(withJWT_SECRET) is part of the deployment.- The frontend build must be present in
frontend/.
Testing
Run everything (single command)
./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
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: 53/53 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.phpis 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.
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.
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 /loginwith{"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
localStorageon the frontend and attached via an axios request interceptor. - Roles:
admin(full access) anduser(read-only). Admin routes live under/admin/.... - Frontend dev login page: http://localhost:5173
API Endpoints
Public / all authenticated users:
POST /loginGET /scoresGET /scores/:idGET /scores/:id/instruments[?piece=N]GET /pieces/:scoreIdGET /download/:path(token viaAuthorizationheader or?token=)
Admin only (role === 'admin'):
POST /admin/scoresPUT /admin/scores/:idPUT /admin/scores/:id/piecesDELETE /admin/scores/:idPOST /admin/scores/:id/uploadGET /admin/scores/:id/filesDELETE /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. 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:
[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.
Code Style
PHP
<?phpopening tags, PascalCase classes, camelCase methods.- 3-space indentation, opening brace on the same line (legacy style).
- Consistent with surrounding code; prefer
include_once/require_oncefor includes. - No database layer: file operations via
ScoreScanner.php.
JavaScript / Svelte
- Svelte 5 runes (
$state,$props,$derived,$effect); prefer$app/stateover 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_SECRETis strong and unique (openssl rand -base64 32); never commitapi/.env.api/config/users.jsonuses non-default credentials (changeadmin/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.phppasses 53/53.npm auditreviewed.- No secrets in code or git history.
Manual checks:
# 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.phpno longer serves files statically (.env/config/users.jsonare not reachable);deleteScoreFileenforcesrealpath()containment and only deletesapi/.envandpartitions/.envare untracked/gitignored (rotate the secret since it remains in git history); JWT uses base64url, requiresexpand validatesalg; credentials errors are generic; theressourcelink rejects non-http(s) URLs.Known findings still open: default
admin/passwordcredentials; login rate limiter is race-prone (noflock) and/api/loginbypasses it; CORS falls back to*for unknown origins;GET /admin/scores/:idpasses extra arguments dropped byScoreScanner::updateScore(soressourceis not persisted); watermarking only keeps page 1 of multi-page PDFs;createScoreanduploadPdfdo not validate path segments;updateScore/updatePiecesdo not sanitize INI values.
Temporary Files
- Put temporary scripts and outputs in
_builds/(gitignored). scripts/convert_final_v2.jsis the only committed script inscripts/.
Git
- Default branch:
master. - No CI/CD configured.
- Only commit when explicitly requested.