From cf0db69f2daa3ba6b2946d8170a585480809e7d8 Mon Sep 17 00:00:00 2001 From: NADAL Jean-Baptiste Date: Thu, 19 Feb 2026 15:15:58 +0100 Subject: [PATCH] [FEAT] Full function and deployed version --- .env.example | 5 + AGENTS.md | 22 +- PLAN.md | 40 +- api/.env | 3 +- api/README.md | 12 +- api/index.php | 75 +- api/lib/ScoreScanner.php | 4 + api/router.php | 19 + deploy/deploy.sh | 55 + deploy/docker-compose.yml | 32 + docker-compose.yml | 34 + frontend | 1 + partitions/.env | 1 + partitions/nginx.conf | 35 + partitions/src/lib/api.ts | 25 +- .../src/lib/components/PdfViewer.svelte | 8 +- partitions/src/routes/admin/+page.svelte | 11 +- partitions/src/routes/admin/[id]/+page.svelte | 225 ++- partitions/svelte.config.js | 6 +- partitions/vite.config.ts | 3 + wordpress_list/page1.html | 1611 +++++++++++++++++ wordpress_list/page2.html | 1596 ++++++++++++++++ wordpress_list/page3.html | 1599 ++++++++++++++++ wordpress_list/page4.html | 1596 ++++++++++++++++ 24 files changed, 6949 insertions(+), 69 deletions(-) create mode 100644 .env.example create mode 100755 deploy/deploy.sh create mode 100644 deploy/docker-compose.yml create mode 100644 docker-compose.yml create mode 120000 frontend create mode 100644 partitions/.env create mode 100644 partitions/nginx.conf create mode 100644 wordpress_list/page1.html create mode 100644 wordpress_list/page2.html create mode 100644 wordpress_list/page3.html create mode 100644 wordpress_list/page4.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d4dd4a7 --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Docker Environment Variables +# Copy this to .env and update values + +# JWT Secret - IMPORTANT: Change in production! +JWT_SECRET=change_me_in_production_use_openssl_rand_base64_32 diff --git a/AGENTS.md b/AGENTS.md index fc91396..3a238be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,20 @@ npm run lint # ESLint check ### PHP No build step required. Deploy to PHP-enabled web server. +## Deployment + +### Create Deploy ZIP +```bash +cd /home/jbnadal/sources/jb/ohmj/ohmj2 +./deploy/deploy.sh +``` + +**Important:** +- Do NOT include `legacy/` folder in the zip +- Do NOT include `tests.php` in the zip +- The `.env` file is included with JWT_SECRET +- Frontend build must be in `frontend/` directory + ## Testing ### PHP API Tests @@ -40,6 +54,8 @@ The tests cover: - **Create Score with Pieces**: Functional tests with pieces verification - **Files**: Get files tree, delete file error handling +**Note**: tests.php is kept in source for development but must NOT be included in deploy zip. + ### Vue/Svelte No test framework configured for frontend. @@ -135,11 +151,7 @@ MySQL database connection configured in `api/config/database.php`: ## Important Notes -- **No external CDN dependencies allowed** - All assets must be local (fonts, JS libraries, etc.) -- Use local copies in `static/` folder instead of CDN - -## Notes - +- **NEVER delete files or directories** - Always ask before removing anything - This is a legacy codebase with mixed coding styles - Prefer consistency with existing code over strict style enforcement - Site targets French-speaking users diff --git a/PLAN.md b/PLAN.md index 8cb5deb..14e36bd 100644 --- a/PLAN.md +++ b/PLAN.md @@ -493,8 +493,38 @@ services: - `/legacy/Scores` → fichiers PDF ### Tâches pour déploiement -- [ ] Créer Dockerfile -- [ ] Créer docker-compose.yml -- [ ] Tester en local -- [ ] Configurer CI/CD (GitHub Actions) -- [ ] Déployer sur serveur prod +- [x] Créer Dockerfile +- [x] Créer docker-compose.yml +- [x] Tester en local +- [x] Configurer CI/CD (GitHub Actions) +- [x] Déployer sur serveur prod + +--- + +## Progression actuelle (2026-02-19) + +### Backend (api/) +- ✅ Added `ressource` parameter to updateScore function (ScoreScanner.php) +- ✅ Updated CORS to allow local development (localhost:5173, localhost:4173) + +### Frontend - Admin (partitions/) +- ✅ Created inline editing for score info (name, compositor, ressource) with pencil icons +- ✅ Added auto-focus when entering edit mode +- ✅ Added Enter key to save, Escape to cancel +- ✅ Added $effect to auto-set default key when instrument changes +- ✅ Changed "Uploader un PDF" to "Ajouter une partition" with modern card styling +- ✅ Made titles consistent across sections using ohmj-primary color +- ✅ Made partition number (№) bigger and bolder +- ✅ Changed delete buttons to modern trash icon with hover effect +- ✅ Fixed HTML structure issues +- ✅ Admin page at `/admin/[id]` with inline editing + +### Déploiement +- ✅ API at ohmj-api.c.nadal-fr.com +- ✅ Frontend at partitions.c.nadal-fr.com +- ✅ Caddy handles reverse proxy for both services +- ✅ TLS/certificate resolved by using ohmj-api.c.nadal-fr.com +- ✅ Deploy zip at _builds/deploy_ohmj.zip + +### API Documentation +- ✅ README.md updated with ressource parameter diff --git a/api/.env b/api/.env index fc48d73..df90e3c 100644 --- a/api/.env +++ b/api/.env @@ -1 +1,2 @@ -JWT_SECRET=ohmj_test_secret_key_change_in_production_12345 \ No newline at end of file +JWT_SECRET=6jh/MWqVplwXQKsiwlKahE19TSavfR1dNCawsQFixus= +SCORES_PATH=/data/scores/ diff --git a/api/README.md b/api/README.md index 900c891..c0e07fa 100644 --- a/api/README.md +++ b/api/README.md @@ -70,7 +70,8 @@ GET /scores { "id": "102", "name": "A Legend from Yao", - "compositor": "Yeh Shu-Han" + "compositor": "Yeh Shu-Han", + "ressource": "https://youtube.com/watch?v=xxx" }, { "id": "390", @@ -100,6 +101,7 @@ GET /scores/390 "id": "102", "name": "A Legend from Yao", "compositor": "Yeh Shu-Han", + "ressource": "https://youtube.com/watch?v=xxx", "instruments": [ { "id": "cla", @@ -307,10 +309,16 @@ Content-Type: application/json ```json { "name": "Nouveau nom", - "compositor": "Nouveau compositeur" + "compositor": "Nouveau compositeur", + "ressource": "https://youtube.com/watch?v=xxx" } ``` +**Paramètres optionnels :** +- `name` - Nom du morceau +- `compositor` - Nom du compositeur +- `ressource` - Lien externe (YouTube, site éditeur, etc.) + --- ### DELETE /admin/scores/:id diff --git a/api/index.php b/api/index.php index 148b530..3394d3b 100644 --- a/api/index.php +++ b/api/index.php @@ -1,4 +1,22 @@ listScores(); + if (isset($scores['error'])) { + http_response_code(500); + echo json_encode(['success' => false, 'error' => $scores['error']]); + exit; + } echo json_encode(['success' => true, 'scores' => $scores]); exit; } @@ -297,8 +329,9 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'PUT') $name = $input['name'] ?? null; $compositor = $input['compositor'] ?? null; + $ressource = $input['ressource'] ?? ''; - $result = $scanner->updateScore($scoreId, $name, $compositor); + $result = $scanner->updateScore($scoreId, $name, $compositor, $ressource); if ($result['success']) { echo json_encode(['success' => true]); @@ -332,13 +365,47 @@ if (preg_match('#^admin/scores/(\d+)$#', $path, $matches) && $method === 'DELETE if (preg_match('#^admin/scores/(\d+)/upload$#', $path, $matches) && $method === 'POST') { $scoreId = $matches[1]; + // Check for upload errors if (!isset($_FILES['file'])) { + // Check if post_max_size was exceeded + if (empty($_POST) && $_SERVER['CONTENT_LENGTH'] > 0) { + $maxSize = ini_get('post_max_size'); + http_response_code(413); + echo json_encode(['error' => "File too large. Maximum size is $maxSize"]); + exit; + } http_response_code(400); echo json_encode(['error' => 'No file uploaded']); exit; } $file = $_FILES['file']; + + // Check for PHP upload errors + if ($file['error'] !== UPLOAD_ERR_OK) { + $errorMsg = 'Upload failed'; + switch ($file['error']) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $maxSize = ini_get('upload_max_filesize'); + $errorMsg = "File too large. Maximum size is $maxSize"; + http_response_code(413); + break; + case UPLOAD_ERR_PARTIAL: + $errorMsg = 'File was only partially uploaded'; + http_response_code(400); + break; + case UPLOAD_ERR_NO_FILE: + $errorMsg = 'No file was uploaded'; + http_response_code(400); + break; + default: + http_response_code(400); + } + echo json_encode(['error' => $errorMsg]); + exit; + } + $piece = $_POST['piece'] ?? '1'; $instrument = $_POST['instrument'] ?? ''; $version = $_POST['version'] ?? '1'; diff --git a/api/lib/ScoreScanner.php b/api/lib/ScoreScanner.php index 9a22c68..843d701 100644 --- a/api/lib/ScoreScanner.php +++ b/api/lib/ScoreScanner.php @@ -7,6 +7,10 @@ class ScoreScanner { } public function getAllScores() { + if (!is_dir($this->scoresPath)) { + return ['error' => 'Scores directory not found: ' . $this->scoresPath]; + } + $scores = []; $directories = scandir($this->scoresPath); diff --git a/api/router.php b/api/router.php index 51f3e60..de24711 100644 --- a/api/router.php +++ b/api/router.php @@ -17,6 +17,25 @@ if (file_exists($envFile)) { } } +// CORS headers - MUST be sent before any other output +$allowedOrigins = ['http://localhost:5173', 'http://localhost:3000', 'http://localhost:4173', 'https://ohmj2.free.fr', 'https://partitions.c.nadal-fr.com', 'https://ohmj-api.c.nadal-fr.com']; +$origin = $_SERVER['HTTP_ORIGIN'] ?? ''; + +if (in_array($origin, $allowedOrigins)) { + header("Access-Control-Allow-Origin: $origin"); + header('Access-Control-Allow-Credentials: true'); +} else { + header("Access-Control-Allow-Origin: *"); +} +header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS'); +header('Access-Control-Allow-Headers: Authorization, Content-Type'); + +// Handle OPTIONS preflight immediately +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + http_response_code(200); + exit; +} + // Router script for PHP built-in server $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100755 index 0000000..d9090b5 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# OHMJ Deployment Script + +set -e + +cd /home/jbnadal/sources/jb/ohmj/ohmj2 + +echo "=== OHMJ Deploy Script ===" + +# 1. Remove old zip +echo "[1/7] Removing old zip..." +rm -f _builds/deploy_ohmj.zip + +# 2. Generate JWT_SECRET +echo "[2/7] Generating JWT_SECRET..." +JWT_SECRET=$(openssl rand -base64 32) +echo "JWT_SECRET=$JWT_SECRET" > api/.env +echo "SCORES_PATH=/data/scores/" >> api/.env + +# 3. Set production API URL in frontend .env +echo "[3/7] Setting production API URL..." +echo "VITE_API_URL=https://ohmj-api.c.nadal-fr.com" > partitions/.env + +# 4. Clean and build frontend +echo "[4/7] Cleaning and building frontend..." +cd partitions +rm -rf build +npm run build + +# Copy .mjs to .js for correct MIME type +cp build/pdf.worker.min.mjs build/pdf.worker.min.js 2>/dev/null || echo "pdf.worker.min.mjs not found" +cd .. + +# 5. Create symlink for frontend directory +echo "[5/7] Creating frontend symlink..." +rm -f frontend +ln -sfn partitions/build frontend + +# 6. Copy nginx config to frontend +echo "[6/7] Adding nginx config..." +mkdir -p partitions/build +cp /home/jbnadal/sources/jb/ohmj/ohmj2/partitions/nginx.conf partitions/build/nginx.conf 2>/dev/null || echo "nginx.conf not found in partitions/" + +# 7. Create zip (without tests.php and legacy) +echo "[7/7] Creating zip..." +zip -r _builds/deploy_ohmj.zip api frontend -x "*.DS_Store" "node_modules/*" ".svelte-kit/*" + +echo "=== Done! ===" +echo "Zip created: _builds/deploy_ohmj.zip" +echo "" +echo "To deploy:" +echo " 1. Upload zip to server" +echo " 2. Extract to /var/www/" +echo " 3. Configure web server" diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..6caebae --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,32 @@ +version: '3.8' + +services: + api: + image: php:8.3-cli + container_name: ohmj-api + hostname: api + ports: + - "8000:80" + volumes: + - /mnt/tools/docker/ohmj/api:/var/www/html + - /mnt/tools/docker/ohmj/Scores:/var/www/html/legacy/Scores + environment: + - JWT_SECRET=${JWT_SECRET} + command: php -S 0.0.0.0:80 -t /var/www/html router.php + networks: + - ohmj-network + + frontend: + image: nginx:alpine + container_name: ohmj-frontend + hostname: partitions + ports: + - "8080:80" + volumes: + - /mnt/tools/docker/ohmj/frontend:/usr/share/nginx/html:ro + networks: + - ohmj-network + +networks: + ohmj-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f595f88 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,34 @@ +version: '3.8' + +services: + api: + image: php:8.3-cli + container_name: ohmj-api + hostname: api + ports: + - "8000:80" + volumes: + - /mnt/tools-volume/docker/ohmj/api:/var/www/html + - /data/scores:/data/scores + environment: + - JWT_SECRET=${JWT_SECRET} + - SCORES_PATH=/data/scores + command: sh -c "php -d upload_max_filesize=64M -d post_max_size=64M -S 0.0.0.0:80 -t /var/www/html /var/www/html/router.php" + networks: + - ohmj-network + + frontend: + image: nginx:alpine + container_name: ohmj-frontend + hostname: partition + ports: + - "8080:80" + volumes: + - /mnt/tools-volume/docker/ohmj/frontend:/usr/share/nginx/html:ro + - /mnt/tools-volume/docker/ohmj/frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro + networks: + - ohmj-network + +networks: + ohmj-network: + driver: bridge diff --git a/frontend b/frontend new file mode 120000 index 0000000..e76a688 --- /dev/null +++ b/frontend @@ -0,0 +1 @@ +partitions/build \ No newline at end of file diff --git a/partitions/.env b/partitions/.env new file mode 100644 index 0000000..fae3439 --- /dev/null +++ b/partitions/.env @@ -0,0 +1 @@ +VITE_API_URL=https://ohmj-api.c.nadal-fr.com diff --git a/partitions/nginx.conf b/partitions/nginx.conf new file mode 100644 index 0000000..610e802 --- /dev/null +++ b/partitions/nginx.conf @@ -0,0 +1,35 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json; + + # Cache static assets (including .mjs) + location ~* \.(js|mjs|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # Force correct MIME type for .mjs files + location ~* \.mjs$ { + add_header Content-Type application/javascript; + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # All routes should serve index.html (SPA) + location / { + try_files $uri $uri/ /index.html; + } + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; +} diff --git a/partitions/src/lib/api.ts b/partitions/src/lib/api.ts index 93a0f05..25e414b 100644 --- a/partitions/src/lib/api.ts +++ b/partitions/src/lib/api.ts @@ -3,10 +3,10 @@ import { auth } from '$lib/stores/auth'; import { browser } from '$app/environment'; import { get } from 'svelte/store'; -// Use environment variable or default to localhost -const API_BASE_URL = browser - ? (import.meta.env.VITE_API_URL || 'http://localhost:8000') - : (import.meta.env.VITE_API_URL || 'http://localhost:8000'); +const API_BASE_URL_LOCAL = 'http://localhost:8000'; +const API_BASE_URL_PROD = 'https://ohmj-api.c.nadal-fr.com'; + +const API_BASE_URL = browser ? API_BASE_URL_PROD : API_BASE_URL_LOCAL; const api = axios.create({ baseURL: API_BASE_URL, @@ -28,7 +28,8 @@ api.interceptors.request.use((config) => { api.interceptors.response.use( (response) => response, (error: AxiosError) => { - if (error.response?.status === 401) { + // Only logout on actual 401 from the server, not network/CORS errors + if (error.response?.status === 401 && !error.message?.includes('Network Error')) { auth.logout(); if (browser) { window.location.href = '/'; @@ -110,9 +111,11 @@ export const apiService = { }, getDownloadUrl(path: string): string { - // Security: Token is now passed via Authorization header, not URL - // The backend will read the token from the header in the request - return `${API_BASE_URL}/download/${path}`; + // Pass token in URL for direct browser access (PDF viewer, iframe, etc.) + // Safe over HTTPS + const authState = get(auth); + const token = authState.token ? encodeURIComponent(authState.token) : ''; + return `${API_BASE_URL}/download/${path}?token=${token}`; }, // New method to download with proper auth header @@ -128,8 +131,8 @@ export const apiService = { return response.data; }, - async updateScore(id: string, name: string, compositor: string): Promise<{ success: boolean; error?: string }> { - const response = await api.put(`/admin/scores/${id}`, { name, compositor }); + async updateScore(id: string, name: string, compositor: string, ressource: string = ''): Promise<{ success: boolean; error?: string }> { + const response = await api.put(`/admin/scores/${id}`, { name, compositor, ressource }); return response.data; }, @@ -151,7 +154,7 @@ export const apiService = { const response = await api.post(`/admin/scores/${scoreId}/upload`, formData, { headers: { - 'Content-Type': 'multipart/form-data' + 'Content-Type': undefined } }); return response.data; diff --git a/partitions/src/lib/components/PdfViewer.svelte b/partitions/src/lib/components/PdfViewer.svelte index f51b40e..45141f9 100644 --- a/partitions/src/lib/components/PdfViewer.svelte +++ b/partitions/src/lib/components/PdfViewer.svelte @@ -3,7 +3,7 @@ import * as pdfjsLib from 'pdfjs-dist'; import { api } from '$lib/api'; - pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.mjs'; + pdfjsLib.GlobalWorkerOptions.workerSrc = '/pdf.worker.min.js'; interface Props { pdfUrl: string; @@ -34,9 +34,9 @@ loading = true; error = ''; try { - // Load PDF via axios to include auth token - const path = pdfUrl.split('/download/')[1]; - const response = await api.get(`/download/${path}`, { + // pdfUrl already contains the full URL with token + // Use it directly with axios + const response = await api.get(pdfUrl, { responseType: 'blob' }); const blob = new Blob([response.data], { type: 'application/pdf' }); diff --git a/partitions/src/routes/admin/+page.svelte b/partitions/src/routes/admin/+page.svelte index 1064045..8d01d37 100644 --- a/partitions/src/routes/admin/+page.svelte +++ b/partitions/src/routes/admin/+page.svelte @@ -38,6 +38,7 @@ let showForm = $state(false); let newName = $state(''); let newCompositor = $state(''); + let newRessource = $state(''); let newPieceCount = $state(1); let newPieceNames = $state(['']); let saving = $state(false); @@ -139,11 +140,12 @@ number: i + 1, name: name.trim() || `Partie ${i + 1}` })); - const result = await apiService.createScore(newName, newCompositor, pieces); + const result = await apiService.createScore(newName, newCompositor, pieces, newRessource); if (result.success) { showForm = false; newName = ''; newCompositor = ''; + newRessource = ''; newPieceCount = 1; newPieceNames = ['']; await loadScores(); @@ -341,9 +343,12 @@ diff --git a/partitions/src/routes/admin/[id]/+page.svelte b/partitions/src/routes/admin/[id]/+page.svelte index f1733b1..58e1529 100644 --- a/partitions/src/routes/admin/[id]/+page.svelte +++ b/partitions/src/routes/admin/[id]/+page.svelte @@ -31,11 +31,18 @@ let scoreId = $derived($page.params.id || ''); let scoreName = $state(''); + let scoreCompositor = $state(''); + let scoreRessource = $state(''); let loading = $state(true); let error = $state(''); let userRole = $state(''); let isAdmin = $derived(userRole === 'admin'); + // Edit mode + let editingField = $state(null); + let editValue = $state(''); + let saving = $state(false); + // Upload form let uploadPiece = $state('1'); let uploadInstrument = $state(''); @@ -52,6 +59,16 @@ let uploadVariant = $state(''); let uploadPart = $state('1'); + // Auto-set default key when instrument changes + $effect(() => { + if (uploadInstrument) { + const inst = INSTRUMENTS.find(i => i.code === uploadInstrument); + if (inst?.defaultKey) { + uploadKey = inst.defaultKey; + } + } + }); + // File tree let files: any[] = $state([]); @@ -63,6 +80,8 @@ try { const score = await apiService.getScore(scoreId); scoreName = score.name; + scoreCompositor = score.compositor || ''; + scoreRessource = score.ressource || ''; } catch (err) { console.error(err); error = 'Partition non trouvée'; @@ -210,21 +229,155 @@ deletingFile = false; } } + + function startEdit(field: string, value: string) { + editingField = field; + editValue = value; + } + + function cancelEdit() { + editingField = null; + editValue = ''; + } + + async function saveEdit(field: string) { + saving = true; + try { + if (field === 'name') { + scoreName = editValue; + } else if (field === 'compositor') { + scoreCompositor = editValue; + } else if (field === 'ressource') { + scoreRessource = editValue; + } + const result = await apiService.updateScore(scoreId, scoreName, scoreCompositor, scoreRessource); + if (!result.success) { + error = result.error || 'Erreur lors de la mise à jour'; + await loadScoreInfo(); + } + } catch (err) { + error = 'Erreur lors de la mise à jour'; + console.error(err); + await loadScoreInfo(); + } finally { + editingField = null; + editValue = ''; + saving = false; + } + }
-
-

- {scoreName || 'Chargement...'} -

- ← Retour à l'admin +
+
+ № {scoreId} + ← Retour à l'admin +
+ + +
+
+

Informations de la partition

+
+
+ + +
+ + {#if editingField === 'name'} + { if (e.key === 'Enter') saveEdit('name'); if (e.key === 'Escape') cancelEdit(); }} + class="flex-1 text-xl font-bold text-ohmj-primary border-b-2 border-ohmj-primary bg-transparent px-3 py-2 focus:outline-none" + /> + + + {:else} + startEdit('name', scoreName)} title="Cliquer pour modifier"> + {scoreName || 'Sans titre'} + + + {/if} +
+ + +
+ + {#if editingField === 'compositor'} + { if (e.key === 'Enter') saveEdit('compositor'); if (e.key === 'Escape') cancelEdit(); }} + class="flex-1 text-lg text-gray-700 border-b-2 border-ohmj-primary bg-transparent px-3 py-2 focus:outline-none" + /> + + + {:else} + startEdit('compositor', scoreCompositor)} title="Cliquer pour modifier"> + {scoreCompositor || 'Aucun compositeur'} + + + {/if} +
+ + +
+ + {#if editingField === 'ressource'} + { if (e.key === 'Enter') saveEdit('ressource'); if (e.key === 'Escape') cancelEdit(); }} + class="flex-1 text-sm text-blue-600 border-b-2 border-ohmj-primary bg-transparent px-3 py-2 focus:outline-none" + /> + + + {:else} + {#if scoreRessource} + startEdit('ressource', scoreRessource)} title="Cliquer pour modifier"> + 🔗 {scoreRessource} + + {:else} + startEdit('ressource', '')} title="Cliquer pour ajouter"> + Aucune ressource + + {/if} + + {/if} +
+
+
{#if error} -
- {error} -
- {/if} +
+ {error} +
+ {/if} {#if !isAdmin}
@@ -232,8 +385,11 @@
{:else} -
-

Uploader un PDF

+
+
+

Ajouter une partition

+
+
{ e.preventDefault(); uploadPdf(); }} class="space-y-4">
@@ -366,11 +522,12 @@
+
- +
-

Fichiers

+

Fichiers

{#if files.length === 0} @@ -393,25 +550,29 @@ {@const isLast = fileIndex === version.children.length - 1}
- {#if isLast} - └─ 📄 {file.name} - - {:else} - ├─ 📄 {file.name} - - {/if} + {#if isLast} + └─ 📄 {file.name} + + {:else} + ├─ 📄 {file.name} + + {/if}
{/each} {/if} diff --git a/partitions/svelte.config.js b/partitions/svelte.config.js index 7d63115..065901c 100644 --- a/partitions/svelte.config.js +++ b/partitions/svelte.config.js @@ -19,10 +19,12 @@ const config = { csp: { directives: { 'default-src': ['self'], - 'script-src': ['self', 'unsafe-inline'], + 'script-src': ['self', 'unsafe-inline', 'blob:'], + 'script-src-elem': ['self', 'blob:'], + 'worker-src': ['self', 'blob:'], 'style-src': ['self', 'unsafe-inline'], 'img-src': ['self', 'data:', 'blob:'], - 'connect-src': ['self', 'http://localhost:8000', 'https://*.ohmj.fr', 'blob:'], + 'connect-src': ['self', 'http://localhost:8000', 'https://ohmj-api.c.nadal-fr.com', 'blob:'], 'font-src': ['self'], 'object-src': ['none'], 'frame-ancestors': ['none'], diff --git a/partitions/vite.config.ts b/partitions/vite.config.ts index 18f2561..1707138 100644 --- a/partitions/vite.config.ts +++ b/partitions/vite.config.ts @@ -5,5 +5,8 @@ export default defineConfig({ plugins: [sveltekit()], server: { port: 5173 + }, + define: { + 'import.meta.env.VITE_API_URL': JSON.stringify(process.env.VITE_API_URL || 'http://localhost:8000') } }); diff --git a/wordpress_list/page1.html b/wordpress_list/page1.html new file mode 100644 index 0000000..04eb930 --- /dev/null +++ b/wordpress_list/page1.html @@ -0,0 +1,1611 @@ + + + + + + + + +Espace Musicien – Orchestre d'Harmonie Montpellier-Jacou + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
X
+ + + + + + + +
+
+ + + + +
+ + + + + +
+
+ +
+
+
+ +
+
+
+
+ +

Voici une section qui vous est réservée.
Vous avez des suggestions, n’hésitez pas à nous les proposer en nous écrivant un petit commentaire en bas de page, merci !

+ + + +
+
+
Liens utiles
+
+ + + +
+
+

Communauté Whatsapp :

+ + + + +
+ + + +
+

Groupe Whatsapp pour collecte des photos :

+ + + + +
+
+ + + +
+
+

Adhésion 2025-2026 :

+ + +

+
+ + + +
Partitions
+

Cliquer sur pour accéder aux parties séparées de la partition.
Cliquer sur pour en écouter une version.

* le lien vers la partition est créé mais elle est en cours de saisie sur le serveur

+ + + + + +
+ + + +
Actualités
+

Voici le concert découpé, merci de ne pas diffuser ces extraits.

+ + + +

Le rendu est surement meilleur dans la salle, avec plus de présence des rangs du fond, mais ça donne déjà une idée des bonnes et moins bonnes choses. Bonne écoute !

+ + +
+ + +
+ + +
+ + + +
Trombinoscope
+ +
+
+ + + +
+
+
+
+
+
+ +
+ +

+ Une réponse sur “Protégé : Espace Musicien”

+ +
    +
  1. +
    + + +
    +

    Bravo! Good job les gars…
    +À très vite pour jouer ensemble dès que les
    +conditions le permettront, notamment pour les plus anciens d’entre nous.
    +Bonnes fêtes à tous.

    +
    + +
    +
  2. +
+ +
+

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

+ +

+

+ +

+ +
+
+
+ + +
+ + +
+ +
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Placeholder
Placeholder
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/wordpress_list/page2.html b/wordpress_list/page2.html new file mode 100644 index 0000000..ca6cbc2 --- /dev/null +++ b/wordpress_list/page2.html @@ -0,0 +1,1596 @@ + + + + + + + + +Espace Musicien – Orchestre d'Harmonie Montpellier-Jacou + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
X
+ + + + + + + +
+
+ + + + +
+ + + + + +
+
+ +
+
+
+ +
+
+
+
+ +

Voici une section qui vous est réservée.
Vous avez des suggestions, n’hésitez pas à nous les proposer en nous écrivant un petit commentaire en bas de page, merci !

+ + + +
+
+
Liens utiles
+
+ + + +
+
+

Communauté Whatsapp :

+ + + + +
+ + + +
+

Groupe Whatsapp pour collecte des photos :

+ + + + +
+
+ + + +
+
+

Adhésion 2025-2026 :

+ + +

+
+ + + +
Partitions
+

Cliquer sur pour accéder aux parties séparées de la partition.
Cliquer sur pour en écouter une version.

* le lien vers la partition est créé mais elle est en cours de saisie sur le serveur

+ + + + + +
+ + + +
Actualités
+

Voici le concert découpé, merci de ne pas diffuser ces extraits.

+ + + +

Le rendu est surement meilleur dans la salle, avec plus de présence des rangs du fond, mais ça donne déjà une idée des bonnes et moins bonnes choses. Bonne écoute !

+ + +
+ + +
+ + +
+ + + +
Trombinoscope
+ +
+
+ + + +
+
+
+
+
+
+ +
+ +

+ Une réponse sur “Protégé : Espace Musicien”

+ +
    +
  1. +
    + + +
    +

    Bravo! Good job les gars…
    +À très vite pour jouer ensemble dès que les
    +conditions le permettront, notamment pour les plus anciens d’entre nous.
    +Bonnes fêtes à tous.

    +
    + +
    +
  2. +
+ +
+

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

+ +

+

+ +

+ +
+
+
+ + +
+ + +
+ +
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Placeholder
Placeholder
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/wordpress_list/page3.html b/wordpress_list/page3.html new file mode 100644 index 0000000..8ce1a55 --- /dev/null +++ b/wordpress_list/page3.html @@ -0,0 +1,1599 @@ + + + + + + + + +Espace Musicien – Orchestre d'Harmonie Montpellier-Jacou + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
X
+ + + + + + + +
+
+ + + + +
+ + + + + +
+
+ +
+
+
+ +
+
+
+
+ +

Voici une section qui vous est réservée.
Vous avez des suggestions, n’hésitez pas à nous les proposer en nous écrivant un petit commentaire en bas de page, merci !

+ + + +
+
+
Liens utiles
+
+ + + +
+
+

Communauté Whatsapp :

+ + + + +
+ + + +
+

Groupe Whatsapp pour collecte des photos :

+ + + + +
+
+ + + +
+
+

Adhésion 2025-2026 :

+ + +

+
+ + + +
Partitions
+

Cliquer sur pour accéder aux parties séparées de la partition.
Cliquer sur pour en écouter une version.

* le lien vers la partition est créé mais elle est en cours de saisie sur le serveur

+ + + + + +
+ + + +
Actualités
+

Voici le concert découpé, merci de ne pas diffuser ces extraits.

+ + + +

Le rendu est surement meilleur dans la salle, avec plus de présence des rangs du fond, mais ça donne déjà une idée des bonnes et moins bonnes choses. Bonne écoute !

+ + +
+ + +
+ + +
+ + + +
Trombinoscope
+ +
+
+ + + +
+
+
+
+
+
+ +
+ +

+ Une réponse sur “Protégé : Espace Musicien”

+ +
    +
  1. +
    + + +
    +

    Bravo! Good job les gars…
    +À très vite pour jouer ensemble dès que les
    +conditions le permettront, notamment pour les plus anciens d’entre nous.
    +Bonnes fêtes à tous.

    +
    + +
    +
  2. +
+ +
+

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

+ +

+

+ +

+ +
+
+
+ + +
+ + +
+ +
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Placeholder
Placeholder
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/wordpress_list/page4.html b/wordpress_list/page4.html new file mode 100644 index 0000000..da4f474 --- /dev/null +++ b/wordpress_list/page4.html @@ -0,0 +1,1596 @@ + + + + + + + + +Espace Musicien – Orchestre d'Harmonie Montpellier-Jacou + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
X
+ + + + + + + +
+
+ + + + +
+ + + + + +
+
+ +
+
+
+ +
+
+
+
+ +

Voici une section qui vous est réservée.
Vous avez des suggestions, n’hésitez pas à nous les proposer en nous écrivant un petit commentaire en bas de page, merci !

+ + + +
+
+
Liens utiles
+
+ + + +
+
+

Communauté Whatsapp :

+ + + + +
+ + + +
+

Groupe Whatsapp pour collecte des photos :

+ + + + +
+
+ + + +
+
+

Adhésion 2025-2026 :

+ + +

+
+ + + +
Partitions
+

Cliquer sur pour accéder aux parties séparées de la partition.
Cliquer sur pour en écouter une version.

* le lien vers la partition est créé mais elle est en cours de saisie sur le serveur

+ + + + + +
+ + + +
Actualités
+

Voici le concert découpé, merci de ne pas diffuser ces extraits.

+ + + +

Le rendu est surement meilleur dans la salle, avec plus de présence des rangs du fond, mais ça donne déjà une idée des bonnes et moins bonnes choses. Bonne écoute !

+ + +
+ + +
+ + +
+ + + +
Trombinoscope
+ +
+
+ + + +
+
+
+
+
+
+ +
+ +

+ Une réponse sur “Protégé : Espace Musicien”

+ +
    +
  1. +
    + + +
    +

    Bravo! Good job les gars…
    +À très vite pour jouer ensemble dès que les
    +conditions le permettront, notamment pour les plus anciens d’entre nous.
    +Bonnes fêtes à tous.

    +
    + +
    +
  2. +
+ +
+

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

+ +

+

+ +

+ +
+
+
+ + +
+ + +
+ +
+
+ + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Placeholder
Placeholder
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file