[FIX] Fix the issue withe percu.

This commit is contained in:
NADAL Jean-Baptiste
2026-09-15 12:11:32 +02:00
parent 71d3b71362
commit 46b2d0630e
7 changed files with 116 additions and 17 deletions
+2 -2
View File
@@ -172,7 +172,7 @@ rm -f /tmp/rate_* 2>/dev/null # clear rate limiting fir
php tests.php
```
- Expected: **183/183 tests passed (100%)**.
- 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.
@@ -346,7 +346,7 @@ joined by `+` (e.g. `contrebasse-sib-clefa-1+baryton-sib-clefa-1`). See `NAMING.
- [ ] 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 183/183.
- [ ] `php tests.php` passes 185/185.
- [ ] `npm audit` reviewed.
- [ ] No secrets in code or git history.
+17
View File
@@ -69,6 +69,23 @@ on ajoute un ou plusieurs instruments, chacun avec **tonalité / clé / partie**
nom est alors construit automatiquement, relié par `+`
(ex. `contrebasse-sib-clefa-1+baryton-sib-clefa-1.pdf`).
### Percussions (`per`)
Comme `sup`/`par`, le dossier `per/` regroupe plusieurs instruments : le **nom du
fichier** précise le(s) instrument(s) de percussion, reliés par `+` lorsqu'ils
partagent la même partition. Noms reconnus : `batterie`, `glockenspiel`,
`xylophone`, `vibraphone`, `timbales`, `grosse_caisse`, `caisse_claire`,
`cymbale`, `triangle`, `tambour`, `tam_tam`, `tom`, `gong`, `bongos`, `maracas`,
`tambourine`, `woodblock`, `claves`.
Exemples : `batterie-1.pdf`, `glockenspiel-1+xylophone-1.pdf`,
`timbales-1+basse-1+batterie-1+triangle-1.pdf`,
`percussion-1+gong-1+cymbalе-1+triangle-1.pdf`.
`percussion` (générique) ne s'utilise que si la partition couvre **tout le
pupitre**. Éviter les noms génériques type `percussion_1`, `percussion_2`, … qui
ne disent pas quel instrument joue.
### Tonalités
`sib` (Si♭), `mib` (Mi♭), `fa` (Fa), `ut` (Do — **`do` s'écrit `ut` dans les
+1 -1
View File
@@ -51,7 +51,7 @@ Détail manuel :
```bash
cd api
rm -f /tmp/rate_* 2>/dev/null
php tests.php # attendu : 183/183
php tests.php # attendu : 185/185
```
Frontend (serveurs API + dev lancés) :
+1 -1
View File
@@ -680,7 +680,7 @@ class ScoreScanner {
$instName = $instrumentNames[$instrument] ?? $instrument;
if (in_array($instrument, ['sup', 'par'], true) && $combined !== null && trim($combined) !== '') {
if (in_array($instrument, ['sup', 'par', 'per'], true) && $combined !== null && trim($combined) !== '') {
// Combined/superposed parts keep their real instrument names (e.g. "contrebasse-1+clarinette-1")
$combined = strtolower(trim($combined));
if (!$this->isValidSegment($combined, '/^[a-z0-9_+.-]+$/')) {
+11
View File
@@ -693,6 +693,17 @@ class APITest {
$this->test('Upload sup combined returns 200', $supUp['code'] === 200);
$this->test('Upload sup combined filename', str_contains($supUp['body']['path'] ?? '', 'contrebasse-1+clarinette-1.pdf'));
// Percussion parts are named after the real instrument(s)
$perUp = $this->multipartRequest("admin/scores/$scoreId/upload", [
['name' => 'file', 'file' => $tmpPdf],
['name' => 'piece', 'data' => '1'],
['name' => 'instrument', 'data' => 'per'],
['name' => 'version', 'data' => '1'],
['name' => 'combined', 'data' => 'glockenspiel-1+xylophone-1'],
]);
$this->test('Upload per combined returns 200', $perUp['code'] === 200);
$this->test('Upload per combined filename', str_contains($perUp['body']['path'] ?? '', 'glockenspiel-1+xylophone-1.pdf'));
// Instruments (list + filter)
$instr = $this->request('GET', "scores/$scoreId/instruments");
$this->test('Instruments returns 200', $instr['code'] === 200);
+11 -3
View File
@@ -203,9 +203,17 @@
</td>
<td class="px-3 py-4 text-sm text-gray-500">{score.compositor || '-'}</td>
<td class="whitespace-nowrap px-3 py-4 text-right text-sm">
<div class="flex justify-end gap-2">
<a href="/admin/{score.id}" class="btn btn-ghost px-2 py-1">
Voir
<div class="flex justify-end gap-1">
<a
href="/admin/{score.id}"
class="rounded-full p-1.5 text-ohmj-primary hover:bg-ohmj-light"
title="Voir la partition"
aria-label="Voir la partition"
>
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</a>
<button
type="button"
+73 -10
View File
@@ -51,15 +51,30 @@
let combinedParts = $state<CombinedPart[]>([
{ instrument: '', key: '', clef: '', part: '1' }
]);
// Percussions (`per`): one or more instruments sharing the part
type PercussionPart = { name: string; part: string };
const PERCUSSIONS = [
'batterie', 'glockenspiel', 'xylophone', 'vibraphone', 'timbales', 'grosse_caisse',
'caisse_claire', 'cymbale', 'triangle', 'tambour', 'tam_tam', 'tom', 'gong',
'bongos', 'maracas', 'tambourine', 'woodblock', 'claves', 'percussion'
];
let percussionParts = $state<PercussionPart[]>([{ name: '', part: '1' }]);
let uploadCombined = $derived(
combinedParts
.filter((p) => p.instrument)
.map((p) =>
[getInstrumentFile(p.instrument), p.key, p.clef, p.part]
.filter((v) => v !== '')
.join('-')
)
.join('+')
uploadInstrument === 'per'
? percussionParts
.filter((p) => p.name)
.map((p) => [p.name, p.part].filter((v) => v !== '').join('-'))
.join('+')
: combinedParts
.filter((p) => p.instrument)
.map((p) =>
[getInstrumentFile(p.instrument), p.key, p.clef, p.part]
.filter((v) => v !== '')
.join('-')
)
.join('+')
);
let uploadWatermarkPosition = $state<'left' | 'right'>('left');
@@ -148,8 +163,8 @@
function getExpectedFilename(): string {
if (!uploadInstrument) return '';
// sup/par: the file is named after the combined instruments (see NAMING.md)
if (uploadInstrument === 'sup' || uploadInstrument === 'par') {
// sup/par/per: the file is named after the real instrument(s) (see NAMING.md)
if (['sup', 'par', 'per'].includes(uploadInstrument)) {
const combined = uploadCombined.trim().toLowerCase();
return combined ? `${combined}.pdf` : '';
}
@@ -540,6 +555,54 @@
</div>
{/if}
{#if uploadInstrument === 'per'}
<div>
<span class="label">Percussions</span>
<div class="mt-1 space-y-2">
{#each percussionParts as p, i}
<div class="flex flex-wrap items-center gap-2">
<select bind:value={p.name} class="input w-48" aria-label="Percussion {i + 1}">
<option value="">Instrument…</option>
{#each PERCUSSIONS as perc}
<option value={perc}>{perc}</option>
{/each}
</select>
<input
bind:value={p.part}
class="input w-20"
placeholder="Partie"
aria-label="Partie percussion {i + 1}"
/>
{#if percussionParts.length > 1}
<button
type="button"
onclick={() => (percussionParts = percussionParts.filter((_, k) => k !== i))}
class="rounded-full p-1.5 text-red-500 hover:bg-red-50 hover:text-red-700"
title="Retirer"
aria-label="Retirer la percussion {i + 1}"
>
<svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
{/if}
</div>
{/each}
</div>
<button
type="button"
onclick={() => (percussionParts = [...percussionParts, { name: '', part: '1' }])}
class="btn btn-ghost mt-2 px-2 py-1"
>
+ Ajouter une percussion
</button>
<p class="mt-1 text-xs text-gray-400">
Instruments de percussion reliés par « + » si plusieurs jouent la même partition
(ex. <code>glockenspiel-1+xylophone-1</code>).
</p>
</div>
{/if}
<div>
<button
type="button"