From d52034712dc33e9b2c7a512bc45b71651a931e7c Mon Sep 17 00:00:00 2001 From: sucupira Date: Thu, 16 Jul 2026 21:58:32 -0400 Subject: [PATCH] fix(app): proxy photo opaque + EXIF strip + WebP (C4), radius_eff communes immenses (A1), variante no-frwiki (A3), cache wiki 24h --- app/package-lock.json | 608 +++++++++++++++++++++- app/package.json | 4 +- app/src/lib/components/PhotoPanel.svelte | 18 +- app/src/lib/components/ResultPanel.svelte | 9 + app/src/lib/components/WikiFiche.svelte | 46 +- app/src/lib/i18n/fr.ts | 8 + app/src/lib/i18n/gcf.ts | 8 + app/src/lib/server/lieux.ts | 54 +- app/src/lib/types.ts | 20 +- app/src/routes/api/photo/[id]/+server.ts | 88 ++++ app/src/routes/api/wiki/[qid]/+server.ts | 24 +- app/src/routes/explorer/+page.svelte | 8 +- app/src/service-worker.ts | 4 +- 13 files changed, 875 insertions(+), 24 deletions(-) create mode 100644 app/src/routes/api/photo/[id]/+server.ts diff --git a/app/package-lock.json b/app/package-lock.json index b0582b5..4bbd374 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -8,18 +8,30 @@ "name": "jwe", "version": "2.0.0", "dependencies": { - "maplibre-gl": "^5.1.0" + "maplibre-gl": "^5.1.0", + "sharp": "^0.35.3" }, "devDependencies": { "@sveltejs/adapter-node": "^5.2.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@types/node": "^26.1.1", "svelte": "^5.0.0", "svelte-check": "^4.1.0", "typescript": "^5.6.0", "vite": "^6.0.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -462,6 +474,506 @@ "node": ">=18" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1246,6 +1758,16 @@ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", "license": "MIT" }, + "node_modules/@types/node": { + "version": "26.1.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", + "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, "node_modules/@types/resolve": { "version": "1.20.2", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", @@ -1364,6 +1886,15 @@ "node": ">=0.10.0" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devalue": { "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", @@ -1879,6 +2410,18 @@ "node": ">=6" } }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", @@ -1886,6 +2429,55 @@ "dev": true, "license": "MIT" }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -2020,6 +2612,13 @@ "node": ">=6" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2034,6 +2633,13 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", diff --git a/app/package.json b/app/package.json index 94dc567..b7fcaea 100644 --- a/app/package.json +++ b/app/package.json @@ -14,12 +14,14 @@ "@sveltejs/adapter-node": "^5.2.0", "@sveltejs/kit": "^2.16.0", "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@types/node": "^26.1.1", "svelte": "^5.0.0", "svelte-check": "^4.1.0", "typescript": "^5.6.0", "vite": "^6.0.0" }, "dependencies": { - "maplibre-gl": "^5.1.0" + "maplibre-gl": "^5.1.0", + "sharp": "^0.35.3" } } diff --git a/app/src/lib/components/PhotoPanel.svelte b/app/src/lib/components/PhotoPanel.svelte index bdbdd34..10ff0da 100644 --- a/app/src/lib/components/PhotoPanel.svelte +++ b/app/src/lib/components/PhotoPanel.svelte @@ -7,10 +7,26 @@ } let { photo, revealed = false }: Props = $props(); + + // Proxy /api/photo : 3 largeurs disponibles, le navigateur choisit (C4). + const srcset = $derived( + photo.srcset + ? Object.entries(photo.srcset) + .map(([w, u]) => `${u} ${w}w`) + .join(', ') + : undefined + );
- {photo.alt} + {photo.alt} {#if revealed}
{photo.credit}
{/if} diff --git a/app/src/lib/components/ResultPanel.svelte b/app/src/lib/components/ResultPanel.svelte index cc3bd89..4b1d394 100644 --- a/app/src/lib/components/ResultPanel.svelte +++ b/app/src/lib/components/ResultPanel.svelte @@ -22,6 +22,9 @@

{#if result.distanceKm !== null}

{t().resultat.distance} : {result.distanceKm} {t().resultat.km}

+ {#if result.radiusEffKm !== null} +

{t().resultat.rayonScore} : {result.radiusEffKm} {t().resultat.km}

+ {/if} {/if}

{result.nom} @@ -91,6 +94,12 @@ color: var(--oki-encre-doux); } + .rayon { + margin: 0.1rem 0 0; + font-size: 0.82rem; + color: var(--oki-encre-doux); + } + .credit-photo { margin: 0; font-size: 0.72rem; diff --git a/app/src/lib/components/WikiFiche.svelte b/app/src/lib/components/WikiFiche.svelte index ba7c496..6181eea 100644 --- a/app/src/lib/components/WikiFiche.svelte +++ b/app/src/lib/components/WikiFiche.svelte @@ -12,8 +12,11 @@ let { nom, wikidataId, tonPositif, tonNeutre }: Props = $props(); - // undefined = chargement, null = pas de fiche (Cas B), WikiSummary = Cas A + // undefined = chargement, null = pas de fiche, WikiSummary = Cas A let wiki = $state(undefined); + // Cas intermédiaire (audit A3) : QID présent, fiche Wikidata existante, + // mais pas d'article Wikipédia en français (404 {reason:'no-frwiki'}). + let sansFrwiki = $state(false); onMount(() => { if (!wikidataId) { @@ -25,7 +28,19 @@ try { const res = await fetch(`/api/wiki/${wikidataId}`); if (annule) return; - wiki = res.ok ? ((await res.json()) as WikiSummary) : null; + if (res.ok) { + wiki = (await res.json()) as WikiSummary; + } else { + if (res.status === 404) { + try { + const corps = (await res.json()) as { reason?: string }; + sansFrwiki = corps.reason === 'no-frwiki'; + } catch { + sansFrwiki = false; + } + } + wiki = null; + } } catch { if (!annule) wiki = null; } @@ -38,6 +53,7 @@ const lienContribution = $derived( `https://fr.wikipedia.org/w/index.php?search=${encodeURIComponent(nom)}&title=Sp%C3%A9cial:Recherche` ); + const lienWikidata = $derived(`https://www.wikidata.org/wiki/${wikidataId}`); {#if wiki === undefined} @@ -61,6 +77,20 @@ {t().resultat.lireWiki} ↗ +{:else if sansFrwiki} + +

{:else}
@@ -169,6 +199,18 @@ background: #b98a2f; } + .ctas { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.9rem; + } + + .lien-wikidata { + color: var(--oki-bleu); + font-weight: 650; + } + .guide { margin: 0.8rem 0 0; font-size: 0.88rem; diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 5ece930..79e14e2 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -57,6 +57,7 @@ const fr = { trouve: 'Tu as trouvé ! Apprends-en plus sur ce lieu.', aDecouvrir: 'Le lieu était ici. Apprends-en plus sur lui.', lireWiki: 'Lire la suite sur Wikipédia', + rayonScore: 'Rayon de notation', commune: 'Commune', region: 'Région', suivant: 'Lieu suivant', @@ -72,6 +73,13 @@ const fr = { guide: 'Besoin d’aide pour démarrer ? Suis le guide OKI « Comment contribuer à Wikipédia ».', guideLien: 'Ouvrir le guide de contribution' }, + lacuneFr: { + titre: 'Pas encore d’article en français', + texte: + 'La fiche Wikidata de ce lieu existe, mais personne n’a encore écrit l’article Wikipédia en français. Tu peux le traduire ou le créer : chaque article compte pour que nos territoires existent dans l’encyclopédie francophone.', + cta: 'Traduire ou créer l’article', + ctaWikidata: 'Voir la fiche Wikidata' + }, defi: { bilan: 'Bilan du défi', scoreTotal: 'Score total', diff --git a/app/src/lib/i18n/gcf.ts b/app/src/lib/i18n/gcf.ts index 772c3d8..2a9d48d 100644 --- a/app/src/lib/i18n/gcf.ts +++ b/app/src/lib/i18n/gcf.ts @@ -59,6 +59,7 @@ const gcf: Dict = { trouve: 'Ou trouvé ! Aprann pli plis asou koté sa-a.', aDecouvrir: 'Koté-la té la. Aprann pli plis asou’y.', lireWiki: 'Li swit-la asou Wikipédia', + rayonScore: 'Réyon pou pwen yo', commune: 'Komin', region: 'Péyi', suivant: 'Koté swivan', @@ -74,6 +75,13 @@ const gcf: Dict = { guide: 'Ou bizwen èd pou koumansé ? Swiv gid OKI « Kijan pou kontribyé sou Wikipédia ».', guideLien: 'Ouvri gid kontribisyon-an' }, + lacuneFr: { + titre: 'Pwen blindé — Pa nyen an fransé', + texte: + 'Fich Wikidata koté sa-a égzisté, mé pésonn pòkò ékri atik Wikipédia-la an fransé. Ou sa tradwi’y oben réyé’y : chak atik ka konté pou téritwa nou égzisté adan ansiklopedi fransé-a.', + cta: 'Tradwi oben réyé atik-la', + ctaWikidata: 'Wè fich Wikidata-la' + }, defi: { bilan: 'Bilan défi-a', scoreTotal: 'Total pwen', diff --git a/app/src/lib/server/lieux.ts b/app/src/lib/server/lieux.ts index 1424ffe..9af7fae 100644 --- a/app/src/lib/server/lieux.ts +++ b/app/src/lib/server/lieux.ts @@ -74,7 +74,14 @@ export function getRandomLieu(options: { return pool[Math.floor(Math.random() * pool.length)]; } -/** Anti-triche : ne garde que les champs publiques pour /api/round. */ +/** Largeurs (px) servies par le proxy photo /api/photo/[id]?w=. */ +export const PHOTO_LARGEURS = [400, 800, 1200] as const; + +/** + * Anti-triche : ne garde que les champs publiques pour /api/round. + * L'URL photo passe par le proxy serveur (/api/photo/) : l'URL Wikimedia + * Commons d'origine contient le nom du fichier, qui révèle souvent le lieu (C4). + */ export function sanitizeRound(lieu: Lieu): RoundPlace { return { id: lieu.id, @@ -82,15 +89,41 @@ export function sanitizeRound(lieu: Lieu): RoundPlace { nom_creole: lieu.nom_creole, categorie: lieu.categorie, difficulte: lieu.difficulte, - photo: lieu.photo, + photo: { + url: `/api/photo/${lieu.id}`, + srcset: { + '400': `/api/photo/${lieu.id}?w=400`, + '800': `/api/photo/${lieu.id}?w=800`, + '1200': `/api/photo/${lieu.id}?w=1200` + }, + credit: lieu.photo.credit, + alt: lieu.photo.alt + }, indices: lieu.indices }; } +/** + * Rayon effectif (km) de la courbe de score (audit A1). + * Les communes guyanaises sont immenses (Saint-Laurent-du-Maroni ≈ 4 321 km²) : + * avec le seul rayon de difficulté, un guess à quelques dizaines de km y est + * injustement quasi nul. Quand `aire_commune_km2` est renseigné, le rayon est + * au moins celui du disque de même aire que la commune : sqrt(aire / π). + * Champ optionnel : sans lui, on retombe sur le rayon de difficulté. + */ +export function rayonEffectifKm(lieu: Lieu): number { + const base = RAYONS[lieu.difficulte]; + const aire = lieu.aire_commune_km2; + if (typeof aire === 'number' && Number.isFinite(aire) && aire > 0) { + return Math.max(base, Math.sqrt(aire / Math.PI)); + } + return base; +} + /** * Scoring recalculé côté serveur : - * score = round(5000 × e^(−d/r)), r selon difficulté ; - * pénalité −10 % par indice ; bonus temps ≤ +20 % ; total plafonné à 5000. + * score = round(5000 × e^(−d/r_eff)) ; pénalité −10 % par indice ; + * bonus temps ≤ +20 % ; total plafonné à 5000. */ export function scoreGuess( lieu: Lieu, @@ -99,7 +132,8 @@ export function scoreGuess( timeMs?: number ): GuessResult { const distanceKm = haversineKm(guess, lieu.coordonnees); - const base = Math.round(MAX_POINTS * Math.exp(-distanceKm / RAYONS[lieu.difficulte])); + const radiusEff = rayonEffectifKm(lieu); + const base = Math.round(MAX_POINTS * Math.exp(-distanceKm / radiusEff)); const penalises = base * Math.max(0, 1 - 0.1 * Math.max(0, hintsUsed)); let total = penalises; if (typeof timeMs === 'number' && Number.isFinite(timeMs) && timeMs >= 0) { @@ -109,6 +143,7 @@ export function scoreGuess( return { distanceKm: Math.round(distanceKm * 10) / 10, score, + radiusEffKm: Math.round(radiusEff * 10) / 10, coordonnees: lieu.coordonnees, commune: lieu.commune, region: lieu.region, @@ -123,6 +158,7 @@ export function skipResult(lieu: Lieu): GuessResult { return { distanceKm: null, score: 0, + radiusEffKm: null, coordonnees: lieu.coordonnees, commune: lieu.commune, region: lieu.region, @@ -132,10 +168,14 @@ export function skipResult(lieu: Lieu): GuessResult { }; } -/** Catalogue Explorer : coords arrondies à 0.05° (mode sans score, fiches autorisées). */ +/** + * Catalogue Explorer : coords arrondies à 0.05° (mode sans score, fiches autorisées). + * L'`id` (uuid) n'est volontairement PAS exposé : il permettrait de corréler + * l'id d'un round avec la commune du lieu (fuite de réponse, audit C4). + * L'Explorer n'a pas besoin d'ids stables (le front utilise l'indice du tableau). + */ export function catalogueExplorer(): ExplorerLieu[] { return lieux.map((l) => ({ - id: l.id, nom: l.nom, nom_creole: l.nom_creole, region: l.region, diff --git a/app/src/lib/types.ts b/app/src/lib/types.ts index f6c3a7b..e2e23b7 100644 --- a/app/src/lib/types.ts +++ b/app/src/lib/types.ts @@ -16,6 +16,11 @@ export interface Photo { url: string; credit: string; alt: string; + /** + * URLs par largeur (px) pour `srcset`. Présent uniquement dans /api/round : + * pointe vers le proxy /api/photo/[id]?w=… (l'URL Commons d'origine n'est jamais exposée). + */ + srcset?: Record<'400' | '800' | '1200', string>; } /** Lieu complet — serveur uniquement, jamais exposé tel quel au client. */ @@ -31,6 +36,12 @@ export interface Lieu { wikidata_id: string | null; photo: Photo; indices: Indice[]; + /** + * Superficie de la commune (km²) — champ optionnel du schéma. + * Sert à élargir le rayon de score pour les communes immenses (ex. Guyane) ; + * le scoring doit tolérer son absence (fallback = rayon de difficulté seul). + */ + aire_commune_km2?: number; } /** Lieu SANITISÉ envoyé par GET /api/round (aucune coordonnée, commune, région, wikidata_id). */ @@ -48,6 +59,8 @@ export interface RoundPlace { export interface GuessResult { distanceKm: number | null; score: number; + /** Rayon effectif utilisé par la courbe de score (km, arrondi à 0,1) — null si « Passer ». */ + radiusEffKm: number | null; coordonnees: Coordonnees; commune: string; region: Region; @@ -56,9 +69,12 @@ export interface GuessResult { wikidata_id: string | null; } -/** Entrée du catalogue Explorer : coords arrondies à 0.05°, mode sans score. */ +/** + * Entrée du catalogue Explorer : coords arrondies à 0.05°, mode sans score. + * Pas d'`id` : exposer l'uuid du lieu permettrait de corréler l'id d'un round + * (/api/round) avec la commune/région — fuite de réponse (audit C4). + */ export interface ExplorerLieu { - id: string; nom: string; nom_creole: string; region: Region; diff --git a/app/src/routes/api/photo/[id]/+server.ts b/app/src/routes/api/photo/[id]/+server.ts new file mode 100644 index 0000000..f5e063f --- /dev/null +++ b/app/src/routes/api/photo/[id]/+server.ts @@ -0,0 +1,88 @@ +import sharp from 'sharp'; +import { getLieuById, PHOTO_LARGEURS } from '$lib/server/lieux'; +import type { RequestHandler } from './$types'; + +const USER_AGENT = 'JWE-OKI/2.0 (https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/JWE; contact OKI)'; +const TTL_MS = 24 * 60 * 60 * 1000; // 24 h + +type Largeur = (typeof PHOTO_LARGEURS)[number]; +const LARGEUR_DEFAUT: Largeur = 1200; + +/** Cache mémoire : clé `${id}:${w}`, TTL 24 h. */ +const cache = new Map(); +/** Déduplication des fetches en vol (même photo demandée en rafale). */ +const enVol = new Map>(); + +/** + * Récupère la photo via l'URL Commons stockée dans le corpus (jamais d'URL + * fournie par le client — pas de proxy ouvert), puis la retraite avec sharp : + * - rotate() : applique l'orientation EXIF avant suppression des métadonnées ; + * - métadonnées : sharp ne conserve AUCUNE métadonnée (EXIF/GPS compris) sauf + * appel explicite à withMetadata() — le WebP produit est donc propre ; + * - conversion WebP + redimensionnement à la largeur demandée. + */ +async function chargerPhoto(id: string, w: Largeur): Promise { + const lieu = getLieuById(id); + if (!lieu) return null; + const res = await fetch(lieu.photo.url, { headers: { 'User-Agent': USER_AGENT } }); + if (!res.ok) throw new Error(`commons ${res.status}`); + const brut = Buffer.from(await res.arrayBuffer()); + return sharp(brut) + .rotate() + .resize({ width: w, withoutEnlargement: true }) + .webp({ quality: 82 }) + .toBuffer(); +} + +/** + * GET /api/photo/[id]?w=400|800|1200 → image/webp. + * `id` = uuid du lieu (déjà exposé par /api/round). 404 si lieu inconnu. + * L'URL ainsi produite est opaque : elle ne révèle ni le nom du fichier + * Commons ni le lieu (audit C4). + */ +export const GET: RequestHandler = async ({ params, url }) => { + const id = params.id; + const wParam = url.searchParams.get('w'); + let w: Largeur = LARGEUR_DEFAUT; + if (wParam !== null) { + const n = Number(wParam); + if (!(PHOTO_LARGEURS as readonly number[]).includes(n)) { + return new Response('Largeur invalide (400, 800 ou 1200)', { status: 400 }); + } + w = n as Largeur; + } + + const cle = `${id}:${w}`; + const hit = cache.get(cle); + if (hit && hit.expires > Date.now()) { + return reponseWebp(hit.data); + } + + // 404 immédiat si l'id est inconnu (avant tout fetch amont). + if (!getLieuById(id)) { + return new Response('Lieu inconnu', { status: 404 }); + } + + try { + let promesse = enVol.get(cle); + if (!promesse) { + promesse = chargerPhoto(id, w).finally(() => enVol.delete(cle)); + enVol.set(cle, promesse); + } + const data = await promesse; + if (!data) return new Response('Lieu inconnu', { status: 404 }); + cache.set(cle, { expires: Date.now() + TTL_MS, data }); + return reponseWebp(data); + } catch { + return new Response('Photo indisponible', { status: 502 }); + } +}; + +function reponseWebp(data: Buffer): Response { + return new Response(new Uint8Array(data), { + headers: { + 'Content-Type': 'image/webp', + 'Cache-Control': 'public, max-age=86400, immutable' + } + }); +} diff --git a/app/src/routes/api/wiki/[qid]/+server.ts b/app/src/routes/api/wiki/[qid]/+server.ts index 7bc6173..e9baa8f 100644 --- a/app/src/routes/api/wiki/[qid]/+server.ts +++ b/app/src/routes/api/wiki/[qid]/+server.ts @@ -4,6 +4,7 @@ import type { RequestHandler } from './$types'; const USER_AGENT = 'JWE-OKI/2.0 (https://labola.o-k-i.net/ORGANISATION-KA-INTERNATIONALE/JWE; contact OKI)'; const TTL_MS = 24 * 60 * 60 * 1000; // 24 h +const CACHE_CONTROL = 'public, max-age=86400'; const cache = new Map(); @@ -23,7 +24,7 @@ interface RestSummary { thumbnail?: { source?: string }; } -/** GET /api/wiki/[qid] → extrait Wikipédia FR normalisé (404 si pas de sitelink fr). */ +/** GET /api/wiki/[qid] → extrait Wikipédia FR normalisé (404 {reason:'no-frwiki'} si pas de sitelink fr). */ export const GET: RequestHandler = async ({ params, fetch }) => { const qid = params.qid; if (!/^Q\d+$/.test(qid)) { @@ -32,26 +33,37 @@ export const GET: RequestHandler = async ({ params, fetch }) => { const cached = cache.get(qid); if (cached && cached.expires > Date.now()) { - return json(cached.summary, { headers: { 'Cache-Control': 'public, max-age=3600' } }); + return json(cached.summary, { headers: { 'Cache-Control': CACHE_CONTROL } }); } const headers = { 'User-Agent': USER_AGENT, Accept: 'application/json' }; // 1. Wikidata : sitelink frwiki (+ libellé fr en fallback) + // Fallback de langue : fr UNIQUEMENT, volontairement sans repli sur en — + // l'absence d'article français devient un appel à contribution (Cas B), pas + // une redirection silencieuse vers Wikipédia en anglais. const wbUrl = `https://www.wikidata.org/w/api.php?action=wbgetentities&ids=${encodeURIComponent(qid)}&props=sitelinks%7Clabels&sitefilter=frwiki&languages=fr&format=json`; let entity: WbEntity | undefined; try { const res = await fetch(wbUrl, { headers }); if (!res.ok) throw new Error(`wikidata ${res.status}`); - entity = ((await res.json()) as WbResponse).entities?.[qid]; + // L'API résout les redirections : l'entité peut être indexée par le QID cible. + const entities = ((await res.json()) as WbResponse).entities ?? {}; + entity = entities[qid] ?? Object.values(entities)[0]; } catch { return json({ erreur: 'Wikidata injoignable' }, { status: 502 }); } const titre = entity?.sitelinks?.frwiki?.title; if (!titre) { - // Pas de page Wikipédia FR → le front bascule sur l'écran « Pwen blindé ». - return json({ erreur: 'Pas de page Wikipédia en français' }, { status: 404 }); + // QID existant mais pas d'article frwiki (cas intermédiaire, audit A3) : + // `reason: no-frwiki` permet au front d'afficher la variante « Pwen blindé — + // Pa nyen an fransé » (fiche Wikidata présente, article FR à traduire/créer), + // distincte du Cas B « lacune totale » (wikidata_id null côté lieu). + return json( + { reason: 'no-frwiki' }, + { status: 404, headers: { 'Cache-Control': CACHE_CONTROL } } + ); } // 2. Wikipédia FR : résumé de l'article @@ -77,5 +89,5 @@ export const GET: RequestHandler = async ({ params, fetch }) => { }; cache.set(qid, { expires: Date.now() + TTL_MS, summary }); - return json(summary, { headers: { 'Cache-Control': 'public, max-age=3600' } }); + return json(summary, { headers: { 'Cache-Control': CACHE_CONTROL } }); }; diff --git a/app/src/routes/explorer/+page.svelte b/app/src/routes/explorer/+page.svelte index 86b0c73..540820b 100644 --- a/app/src/routes/explorer/+page.svelte +++ b/app/src/routes/explorer/+page.svelte @@ -18,7 +18,9 @@ ); const markers = $derived( lieuxFiltres.map((l) => ({ - id: l.id, + // Id éphémère côté client (indice dans le catalogue) : /api/lieux n'expose + // plus l'uuid du lieu, pour ne pas corréler un id de round avec sa commune (C4). + id: String(lieux.indexOf(l)), lat: l.coordonnees.lat, lon: l.coordonnees.lon, label: `${l.nom} — ${l.commune}` @@ -26,7 +28,7 @@ ); function ouvrir(id: string): void { - selection = lieux.find((l) => l.id === id) ?? null; + selection = lieux[Number(id)] ?? null; } onMount(() => { @@ -103,7 +105,7 @@

{t().resultat.commune} : {selection.commune} — {t().regions[selection.region]}

- {#key selection.id} + {#key selection.nom} {/key} {:else} diff --git a/app/src/service-worker.ts b/app/src/service-worker.ts index 49e829b..5a926a5 100644 --- a/app/src/service-worker.ts +++ b/app/src/service-worker.ts @@ -31,7 +31,9 @@ sw.addEventListener('activate', (event) => { function isPhoto(url: URL): boolean { return ( url.hostname === 'upload.wikimedia.org' || - (url.hostname === 'commons.wikimedia.org' && url.pathname.startsWith('/wiki/Special:FilePath')) + (url.hostname === 'commons.wikimedia.org' && url.pathname.startsWith('/wiki/Special:FilePath')) || + // Proxy photo maison (mode jeu) — réponses immutables, WebP sans métadonnées + (url.origin === sw.location.origin && url.pathname.startsWith('/api/photo/')) ); }