fix(app): proxy photo opaque + EXIF strip + WebP (C4), radius_eff communes immenses (A1), variante no-frwiki (A3), cache wiki 24h

This commit is contained in:
sucupira
2026-07-16 21:58:32 -04:00
parent e41a508d77
commit d52034712d
13 changed files with 875 additions and 24 deletions
+607 -1
View File
@@ -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",
+3 -1
View File
@@ -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"
}
}
+17 -1
View File
@@ -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
);
</script>
<figure class="photo-panel">
<img src={photo.url} alt={photo.alt} loading="lazy" decoding="async" />
<img
src={photo.url}
{srcset}
sizes="(min-width: 768px) 50vw, 100vw"
alt={photo.alt}
loading="lazy"
decoding="async"
/>
{#if revealed}
<figcaption>{photo.credit}</figcaption>
{/if}
@@ -22,6 +22,9 @@
</p>
{#if result.distanceKm !== null}
<p class="distance">{t().resultat.distance} : {result.distanceKm} {t().resultat.km}</p>
{#if result.radiusEffKm !== null}
<p class="rayon">{t().resultat.rayonScore} : {result.radiusEffKm} {t().resultat.km}</p>
{/if}
{/if}
<p class="lieu">
<strong>{result.nom}</strong>
@@ -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;
+44 -2
View File
@@ -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<WikiSummary | null | undefined>(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}`);
</script>
{#if wiki === undefined}
@@ -61,6 +77,20 @@
{t().resultat.lireWiki}
</a>
</article>
{:else if sansFrwiki}
<!-- Variante Cas B : fiche Wikidata OK mais pas d'article FR — appel à traduire/créer -->
<article class="panneau cas-b">
<h2><span class="icone" aria-hidden="true"></span> {t().lacuneFr.titre}</h2>
<p class="texte">{t().lacuneFr.texte}</p>
<div class="ctas">
<a class="btn btn-cta" href={lienContribution} target="_blank" rel="noopener noreferrer">
{t().lacuneFr.cta}
</a>
<a class="lien-wikidata" href={lienWikidata} target="_blank" rel="noopener noreferrer">
{t().lacuneFr.ctaWikidata}
</a>
</div>
</article>
{:else}
<!-- Cas B : lacune documentaire — panneau volontairement distinct (Peak-End) -->
<article class="panneau cas-b">
@@ -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;
+8
View File
@@ -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 daide pour démarrer ? Suis le guide OKI « Comment contribuer à Wikipédia ».',
guideLien: 'Ouvrir le guide de contribution'
},
lacuneFr: {
titre: 'Pas encore darticle en français',
texte:
'La fiche Wikidata de ce lieu existe, mais personne na encore écrit larticle Wikipédia en français. Tu peux le traduire ou le créer : chaque article compte pour que nos territoires existent dans lencyclopédie francophone.',
cta: 'Traduire ou créer larticle',
ctaWikidata: 'Voir la fiche Wikidata'
},
defi: {
bilan: 'Bilan du défi',
scoreTotal: 'Score total',
+8
View File
@@ -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 asouy.',
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 tradwiy 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',
+47 -7
View File
@@ -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/<uuid>) : 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,
+18 -2
View File
@@ -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;
+88
View File
@@ -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<string, { expires: number; data: Buffer }>();
/** Déduplication des fetches en vol (même photo demandée en rafale). */
const enVol = new Map<string, Promise<Buffer | null>>();
/**
* 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<Buffer | null> {
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'
}
});
}
+18 -6
View File
@@ -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<string, { expires: number; summary: WikiSummary }>();
@@ -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 } });
};
+5 -3
View File
@@ -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 @@
<p class="localisation">
{t().resultat.commune} : {selection.commune}{t().regions[selection.region]}
</p>
{#key selection.id}
{#key selection.nom}
<WikiFiche nom={selection.nom} wikidataId={selection.wikidata_id} />
{/key}
{:else}
+3 -1
View File
@@ -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/'))
);
}