diff --git a/app.vue b/app.vue
index ee95740..06afe2d 100644
--- a/app.vue
+++ b/app.vue
@@ -39,8 +39,7 @@
class="nav-tab"
:class="{ 'nav-tab--active': route.path === '/agences' }"
>
- Agences Inspirantes
- en construction
+ Réseaux AEP
en construction
+
+ Jobs
+
+
+ Codev
+
@@ -165,8 +178,9 @@
@click="hamburgerOpen = false"
>
Écosystème Entraide Architecture
- Agences Inspirantes
+ Réseaux AEP
RAG
+ Codev
diff --git a/components/codev/CodevGraph.vue b/components/codev/CodevGraph.vue
new file mode 100644
index 0000000..cc4acc8
--- /dev/null
+++ b/components/codev/CodevGraph.vue
@@ -0,0 +1,450 @@
+
+
+
+
+
+
Encore personne. Sois la premiere fiche !
+
Creer ma fiche →
+
+
+
+
+
+
+
+
+
+
+
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 9c62d00..f84f305 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -1,6 +1,11 @@
export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss'],
- css: ['~/assets/css/main.css'],
+ css: [
+ '~/assets/css/main.css',
+ 'leaflet/dist/leaflet.css',
+ 'leaflet.markercluster/dist/MarkerCluster.css',
+ 'leaflet.markercluster/dist/MarkerCluster.Default.css',
+ ],
runtimeConfig: {
nocodbUrl: process.env.NOCODB_URL,
@@ -14,16 +19,20 @@ export default defineNuxtConfig({
redisUrl: process.env.REDIS_URL || 'redis://127.0.0.1:6379',
resendApiKey: process.env.RESEND_API_KEY,
emailJules: process.env.EMAIL_JULES || 'jules@trans-former.fr',
+ codevTableId: '', // NUXT_CODEV_TABLE_ID
+ codevPassword: 'merci', // NUXT_CODEV_PASSWORD - défaut "merci", overridable
+ codevBaseId: '', // NUXT_CODEV_BASE_ID - base NocoDB (ex: pipilvsi7dibo80)
+ codevAdminPassword: 'admin2026', // NUXT_CODEV_ADMIN_PASSWORD
},
// Leaflet ne fonctionne pas en SSR — forcer le rendu côté client
ssr: true,
vite: {
+ cacheDir: 'C:/Users/jules/AppData/Local/nav-carte-vite-cache',
optimizeDeps: {
- include: ['leaflet', 'leaflet.markercluster'],
+ include: ['leaflet', 'leaflet.markercluster', 'd3'],
},
- // Éviter l'import SSR de Leaflet qui utilise window
ssr: {
noExternal: [],
},
diff --git a/package-lock.json b/package-lock.json
index af4ed5b..bdf6d04 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,7 @@
"@headlessui/vue": "^1.7.23",
"@types/leaflet": "^1.9.21",
"@types/leaflet.markercluster": "^1.5.6",
+ "d3": "^7.9.0",
"ioredis": "^5.3.2",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
@@ -5312,6 +5313,416 @@
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
+ "node_modules/d3": {
+ "version": "7.9.0",
+ "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
+ "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "3",
+ "d3-axis": "3",
+ "d3-brush": "3",
+ "d3-chord": "3",
+ "d3-color": "3",
+ "d3-contour": "4",
+ "d3-delaunay": "6",
+ "d3-dispatch": "3",
+ "d3-drag": "3",
+ "d3-dsv": "3",
+ "d3-ease": "3",
+ "d3-fetch": "3",
+ "d3-force": "3",
+ "d3-format": "3",
+ "d3-geo": "3",
+ "d3-hierarchy": "3",
+ "d3-interpolate": "3",
+ "d3-path": "3",
+ "d3-polygon": "3",
+ "d3-quadtree": "3",
+ "d3-random": "3",
+ "d3-scale": "4",
+ "d3-scale-chromatic": "3",
+ "d3-selection": "3",
+ "d3-shape": "3",
+ "d3-time": "3",
+ "d3-time-format": "4",
+ "d3-timer": "3",
+ "d3-transition": "3",
+ "d3-zoom": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-array": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
+ "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
+ "license": "ISC",
+ "dependencies": {
+ "internmap": "1 - 2"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-axis": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
+ "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-brush": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
+ "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "3",
+ "d3-transition": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-chord": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
+ "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-color": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
+ "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-contour": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
+ "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "^3.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-delaunay": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
+ "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
+ "license": "ISC",
+ "dependencies": {
+ "delaunator": "5"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dispatch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
+ "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-drag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
+ "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-selection": "3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
+ "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
+ "license": "ISC",
+ "dependencies": {
+ "commander": "7",
+ "iconv-lite": "0.6",
+ "rw": "1"
+ },
+ "bin": {
+ "csv2json": "bin/dsv2json.js",
+ "csv2tsv": "bin/dsv2dsv.js",
+ "dsv2dsv": "bin/dsv2dsv.js",
+ "dsv2json": "bin/dsv2json.js",
+ "json2csv": "bin/json2dsv.js",
+ "json2dsv": "bin/json2dsv.js",
+ "json2tsv": "bin/json2dsv.js",
+ "tsv2csv": "bin/dsv2dsv.js",
+ "tsv2json": "bin/dsv2json.js"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-dsv/node_modules/commander": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/d3-ease": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
+ "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-fetch": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
+ "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dsv": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-force": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
+ "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-quadtree": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-format": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz",
+ "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-geo": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
+ "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.5.0 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-hierarchy": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
+ "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-interpolate": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
+ "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-path": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
+ "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-polygon": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
+ "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-quadtree": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
+ "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-random": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
+ "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
+ "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2.10.0 - 3",
+ "d3-format": "1 - 3",
+ "d3-interpolate": "1.2.0 - 3",
+ "d3-time": "2.1.1 - 3",
+ "d3-time-format": "2 - 4"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-scale-chromatic": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
+ "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-interpolate": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-selection": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
+ "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-shape": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
+ "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-path": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
+ "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-array": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-time-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
+ "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-time": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-timer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
+ "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/d3-transition": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
+ "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-color": "1 - 3",
+ "d3-dispatch": "1 - 3",
+ "d3-ease": "1 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-timer": "1 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "d3-selection": "2 - 3"
+ }
+ },
+ "node_modules/d3-zoom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
+ "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
+ "license": "ISC",
+ "dependencies": {
+ "d3-dispatch": "1 - 3",
+ "d3-drag": "2 - 3",
+ "d3-interpolate": "1 - 3",
+ "d3-selection": "2 - 3",
+ "d3-transition": "2 - 3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/db0": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/db0/-/db0-0.3.4.tgz",
@@ -5425,6 +5836,15 @@
"integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==",
"license": "MIT"
},
+ "node_modules/delaunator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz",
+ "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==",
+ "license": "ISC",
+ "dependencies": {
+ "robust-predicates": "^3.0.2"
+ }
+ },
"node_modules/delegates": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
@@ -6480,6 +6900,18 @@
"node": ">=16.17.0"
}
},
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -6555,6 +6987,15 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
+ "node_modules/internmap": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
+ "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/ioredis": {
"version": "5.10.1",
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
@@ -9480,6 +9921,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/robust-predicates": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz",
+ "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==",
+ "license": "Unlicense"
+ },
"node_modules/rollup": {
"version": "4.60.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz",
@@ -9595,6 +10042,12 @@
"queue-microtask": "^1.2.2"
}
},
+ "node_modules/rw": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
+ "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -9633,6 +10086,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
"node_modules/sax": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
diff --git a/package.json b/package.json
index 19676b2..c42ee54 100644
--- a/package.json
+++ b/package.json
@@ -13,6 +13,7 @@
"@headlessui/vue": "^1.7.23",
"@types/leaflet": "^1.9.21",
"@types/leaflet.markercluster": "^1.5.6",
+ "d3": "^7.9.0",
"ioredis": "^5.3.2",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
diff --git a/pages/codev/carto.vue b/pages/codev/carto.vue
new file mode 100644
index 0000000..d201e6a
--- /dev/null
+++ b/pages/codev/carto.vue
@@ -0,0 +1,550 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chargement du graphe...
+
+
+
+
+
+
+ Mode {{ MODE_LABELS[mode] }} actif -
+ {{ matches.length }} connexion{{ matches.length !== 1 ? 's' : '' }} trouvee{{ matches.length !== 1 ? 's' : '' }}.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Aucune fiche. Ajouter la mienne
+
+
+
+
+
Clique sur une ligne pour modifier la fiche
+
+
+
+
+ +
+
+
+
+
+
+
+
{{ selectedFiche.nom }}
+
+
Besoin
+
{{ selectedFiche.besoin }}
+
+
+
Ce que j'apporte
+
{{ selectedFiche.offre }}
+
+
+ #{{ t }}
+
+
Modifier cette fiche
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/codev/demo.vue b/pages/codev/demo.vue
new file mode 100644
index 0000000..5e28bfc
--- /dev/null
+++ b/pages/codev/demo.vue
@@ -0,0 +1,383 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Chargement du graphe...
+
+
+
+
+
+
+ Mode {{ MODE_LABELS[mode] }} actif -
+ {{ matches.length }} connexion{{ matches.length !== 1 ? 's' : '' }} trouvee{{ matches.length !== 1 ? 's' : '' }}.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/codev/fiche.vue b/pages/codev/fiche.vue
new file mode 100644
index 0000000..d1a36f1
--- /dev/null
+++ b/pages/codev/fiche.vue
@@ -0,0 +1,415 @@
+
+
+
+
+
+
+
diff --git a/pages/codev/index.vue b/pages/codev/index.vue
new file mode 100644
index 0000000..cb8e4b3
--- /dev/null
+++ b/pages/codev/index.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pages/codev/qr.vue b/pages/codev/qr.vue
new file mode 100644
index 0000000..ebedd8b
--- /dev/null
+++ b/pages/codev/qr.vue
@@ -0,0 +1,94 @@
+
+
+
+
Co-développement
+
Scanne pour rejoindre la session
+
+
![QR code aep.trans-former.fr/codev]()
+
+
{{ APP_URL }}
+
Mot de passe : merci
+
+
+ Télécharger le QR code
+
+
+
+
+
+
+
+
diff --git a/server/api/codev/auth.post.ts b/server/api/codev/auth.post.ts
new file mode 100644
index 0000000..3524490
--- /dev/null
+++ b/server/api/codev/auth.post.ts
@@ -0,0 +1,46 @@
+import { z } from 'zod'
+
+const AuthSchema = z.object({
+ password: z.string().min(1).max(100),
+})
+
+export default defineEventHandler(async (event) => {
+ const body = await readBody(event)
+ const parsed = AuthSchema.safeParse(body)
+
+ if (!parsed.success) {
+ throw createError({ statusCode: 422, statusMessage: 'Mot de passe invalide' })
+ }
+
+ const config = useRuntimeConfig()
+ const expected = config.codevPassword || 'merci'
+
+ const isAdmin = parsed.data.password.trim().toLowerCase() === (config.codevAdminPassword || 'admin2026').trim().toLowerCase()
+ const isUser = parsed.data.password.trim().toLowerCase() === expected.trim().toLowerCase()
+
+ if (!isAdmin && !isUser) {
+ throw createError({ statusCode: 401, statusMessage: 'Mauvais mot de passe' })
+ }
+
+ // Cookie session (user + admin)
+ setCookie(event, 'codev_session', 'ok', {
+ httpOnly: true,
+ sameSite: 'lax',
+ secure: process.env.NODE_ENV === 'production',
+ maxAge: 60 * 60 * 24, // 24h
+ path: '/',
+ })
+
+ // Cookie admin si mot de passe admin
+ if (isAdmin) {
+ setCookie(event, 'codev_admin', 'ok', {
+ httpOnly: true,
+ sameSite: 'lax',
+ secure: process.env.NODE_ENV === 'production',
+ maxAge: 60 * 60 * 24, // 24h
+ path: '/',
+ })
+ }
+
+ return { status: 200, ok: true, admin: isAdmin }
+})
diff --git a/server/api/codev/fiches.get.ts b/server/api/codev/fiches.get.ts
new file mode 100644
index 0000000..cd65f37
--- /dev/null
+++ b/server/api/codev/fiches.get.ts
@@ -0,0 +1,31 @@
+import type { CodevFiche } from '~/types/codev'
+
+export default defineEventHandler(async (event): Promise<{ list: CodevFiche[] }> => {
+ const config = useRuntimeConfig()
+ const tableId = config.codevTableId
+
+ if (!tableId) {
+ throw createError({ statusCode: 500, message: 'codevTableId non configuré' })
+ }
+
+ const url = `${config.nocodbUrl}/api/v2/tables/${tableId}/records?sort=created_at&limit=200`
+
+ const data: any = await $fetch(url, {
+ headers: { 'xc-token': config.nocodbToken },
+ }).catch(() => ({ list: [] }))
+
+ // Mapper chaque record NocoDB vers CodevFiche
+ const list: CodevFiche[] = (data?.list ?? []).map((r: any) => ({
+ id: r.Id ?? r.id,
+ nom: r.nom || '',
+ besoin: r.besoin || '',
+ offre: r.offre || '',
+ hashtags: (r.hashtags || '')
+ .split(',')
+ .map((h: string) => h.trim().toLowerCase().replace(/^#/, ''))
+ .filter(Boolean),
+ created_at: r.created_at || r.CreatedAt || new Date().toISOString(),
+ }))
+
+ return { list }
+})
diff --git a/server/api/codev/fiches.post.ts b/server/api/codev/fiches.post.ts
new file mode 100644
index 0000000..be1d3ce
--- /dev/null
+++ b/server/api/codev/fiches.post.ts
@@ -0,0 +1,63 @@
+import { z } from 'zod'
+
+const FicheSchema = z.object({
+ nom: z.string().min(2).max(50).trim(),
+ besoin: z.string().min(5).max(300).trim(),
+ offre: z.string().min(5).max(300).trim(),
+ hashtags: z.array(z.string().max(30)).max(3).default([]),
+})
+
+export default defineEventHandler(async (event) => {
+ const body = await readBody(event)
+ const parsed = FicheSchema.safeParse(body)
+
+ if (!parsed.success) {
+ throw createError({
+ statusCode: 422,
+ statusMessage: 'Validation échouée',
+ data: parsed.error.flatten().fieldErrors,
+ })
+ }
+
+ const config = useRuntimeConfig()
+ const tableId = config.codevTableId
+ const baseId = config.codevBaseId || 'pipilvsi7dibo80'
+
+ const payload = {
+ nom: parsed.data.nom,
+ besoin: parsed.data.besoin,
+ offre: parsed.data.offre,
+ hashtags: parsed.data.hashtags
+ .map((h) => h.trim().toLowerCase().replace(/^#/, ''))
+ .filter(Boolean)
+ .slice(0, 3)
+ .join(','),
+ created_at: new Date().toISOString(),
+ }
+
+ // NocoDB v1 endpoint pour INSERT (cf. submit/index.post.ts pour le pattern)
+ const insertUrl = `${config.nocodbUrl}/api/v1/db/data/noco/${baseId}/${tableId}`
+
+ let inserted: any
+ try {
+ inserted = await $fetch(insertUrl, {
+ method: 'POST',
+ headers: {
+ 'xc-token': config.nocodbToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ })
+ } catch (e: any) {
+ console.error('[codev/fiches.post] NocoDB insert error:', e?.message ?? e)
+ throw createError({
+ statusCode: 502,
+ statusMessage: 'Erreur serveur, réessaie',
+ })
+ }
+
+ return {
+ status: 201,
+ id: inserted?.Id ?? inserted?.id ?? null,
+ }
+})
diff --git a/server/api/codev/fiches/[id].delete.ts b/server/api/codev/fiches/[id].delete.ts
new file mode 100644
index 0000000..078804b
--- /dev/null
+++ b/server/api/codev/fiches/[id].delete.ts
@@ -0,0 +1,25 @@
+export default defineEventHandler(async (event) => {
+ // Vérif cookie admin
+ const adminCookie = getCookie(event, 'codev_admin')
+ if (adminCookie !== 'ok') {
+ throw createError({ statusCode: 403, statusMessage: 'Accès refusé' })
+ }
+
+ const config = useRuntimeConfig()
+ const tableId = config.codevTableId
+ const id = getRouterParam(event, 'id')
+
+ if (!tableId || !id) {
+ throw createError({ statusCode: 400, message: 'Parametre manquant' })
+ }
+
+ await $fetch(`${config.nocodbUrl}/api/v2/tables/${tableId}/records`, {
+ method: 'DELETE',
+ headers: { 'xc-token': config.nocodbToken, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ Id: Number(id) }),
+ }).catch(() => {
+ throw createError({ statusCode: 502, statusMessage: 'Erreur suppression' })
+ })
+
+ return { status: 200, ok: true }
+})
diff --git a/server/api/codev/fiches/[id].get.ts b/server/api/codev/fiches/[id].get.ts
new file mode 100644
index 0000000..e7425ea
--- /dev/null
+++ b/server/api/codev/fiches/[id].get.ts
@@ -0,0 +1,34 @@
+import type { CodevFiche } from '~/types/codev'
+
+export default defineEventHandler(async (event): Promise
=> {
+ const config = useRuntimeConfig()
+ const tableId = config.codevTableId
+ const baseId = config.codevBaseId || 'pipilvsi7dibo80'
+ const id = getRouterParam(event, 'id')
+
+ if (!tableId || !id) {
+ throw createError({ statusCode: 400, message: 'Parametre manquant' })
+ }
+
+ const url = `${config.nocodbUrl}/api/v1/db/data/noco/${baseId}/${tableId}/${id}`
+
+ const r: any = await $fetch(url, {
+ headers: { 'xc-token': config.nocodbToken },
+ }).catch(() => null)
+
+ if (!r) {
+ throw createError({ statusCode: 404, message: 'Fiche introuvable' })
+ }
+
+ return {
+ id: r.Id ?? r.id,
+ nom: r.nom || '',
+ besoin: r.besoin || '',
+ offre: r.offre || '',
+ hashtags: (r.hashtags || '')
+ .split(',')
+ .map((h: string) => h.trim().toLowerCase().replace(/^#/, ''))
+ .filter(Boolean),
+ created_at: r.created_at || r.CreatedAt || '',
+ }
+})
diff --git a/server/api/codev/fiches/[id].patch.ts b/server/api/codev/fiches/[id].patch.ts
new file mode 100644
index 0000000..80409fe
--- /dev/null
+++ b/server/api/codev/fiches/[id].patch.ts
@@ -0,0 +1,59 @@
+import { z } from 'zod'
+
+const PatchSchema = z.object({
+ nom: z.string().min(2).max(50).trim(),
+ besoin: z.string().min(5).max(300).trim(),
+ offre: z.string().min(5).max(300).trim(),
+ hashtags: z.array(z.string().max(30)).max(3).default([]),
+})
+
+export default defineEventHandler(async (event) => {
+ const config = useRuntimeConfig()
+ const tableId = config.codevTableId
+ const baseId = config.codevBaseId || 'pipilvsi7dibo80'
+ const id = getRouterParam(event, 'id')
+ const body = await readBody(event)
+
+ if (!tableId || !id) {
+ throw createError({ statusCode: 400, message: 'Parametre manquant' })
+ }
+
+ const parsed = PatchSchema.safeParse(body)
+ if (!parsed.success) {
+ throw createError({
+ statusCode: 422,
+ statusMessage: 'Validation echouee',
+ data: parsed.error.flatten().fieldErrors,
+ })
+ }
+
+ const payload = {
+ nom: parsed.data.nom,
+ besoin: parsed.data.besoin,
+ offre: parsed.data.offre,
+ hashtags: parsed.data.hashtags
+ .map((h) => h.trim().toLowerCase().replace(/^#/, ''))
+ .filter(Boolean)
+ .slice(0, 3)
+ .join(','),
+ }
+
+ // NocoDB v1 PATCH par Id
+ const url = `${config.nocodbUrl}/api/v1/db/data/noco/${baseId}/${tableId}/${id}`
+
+ try {
+ await $fetch(url, {
+ method: 'PATCH',
+ headers: {
+ 'xc-token': config.nocodbToken,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(payload),
+ })
+ } catch (e: any) {
+ console.error('[codev/fiches.patch] NocoDB patch error:', e?.message ?? e)
+ throw createError({ statusCode: 502, statusMessage: 'Erreur serveur' })
+ }
+
+ return { status: 200, ok: true }
+})
diff --git a/server/api/codev/me.get.ts b/server/api/codev/me.get.ts
new file mode 100644
index 0000000..ea11412
--- /dev/null
+++ b/server/api/codev/me.get.ts
@@ -0,0 +1,5 @@
+export default defineEventHandler((event) => {
+ const admin = getCookie(event, 'codev_admin') === 'ok'
+ const session = getCookie(event, 'codev_session') === 'ok'
+ return { admin, session }
+})
diff --git a/server/middleware/codev-auth.ts b/server/middleware/codev-auth.ts
new file mode 100644
index 0000000..d04a5cc
--- /dev/null
+++ b/server/middleware/codev-auth.ts
@@ -0,0 +1,21 @@
+// Middleware server Nuxt — protection des routes /codev/fiche et /codev/carto
+// Laisse passer /codev (lock screen), /codev/demo et toutes les routes /api/*
+
+export default defineEventHandler((event) => {
+ const url = getRequestURL(event)
+ const path = url.pathname
+
+ // Seulement les routes sous /codev/
+ if (!path.startsWith('/codev/')) return
+
+ // Routes publiques : /codev/demo et /codev/qr (et sous-routes éventuelles)
+ if (path === '/codev/demo' || path.startsWith('/codev/demo/')) return
+ if (path === '/codev/qr' || path.startsWith('/codev/qr/')) return
+
+ // Vérification cookie
+ const session = getCookie(event, 'codev_session')
+ if (session === 'ok') return
+
+ // Non authentifié -> redirect vers /codev (lock screen)
+ return sendRedirect(event, '/codev', 302)
+})
diff --git a/types/codev.ts b/types/codev.ts
new file mode 100644
index 0000000..c2e1ef7
--- /dev/null
+++ b/types/codev.ts
@@ -0,0 +1,18 @@
+export interface CodevFiche {
+ id: number
+ nom: string
+ besoin: string
+ offre: string
+ hashtags: string[] // parsé depuis CSV NocoDB
+ created_at: string // ISO
+}
+
+export interface CodevMatch {
+ fromId: number
+ toId: number
+ score: number // 0-1
+ mode: 'solution' | 'alliance' | 'surprise'
+ // solution : fromId.besoin matche toId.offre (orienté)
+ // alliance : symétrique sur besoin
+ // surprise : symétrique sur offre
+}
diff --git a/utils/codev/matching.ts b/utils/codev/matching.ts
new file mode 100644
index 0000000..d8dc7e9
--- /dev/null
+++ b/utils/codev/matching.ts
@@ -0,0 +1,106 @@
+import type { CodevFiche, CodevMatch } from '~/types/codev'
+
+const STOP_WORDS_FR = new Set([
+ 'le', 'la', 'les', 'un', 'une', 'des', 'de', 'du', 'au', 'aux',
+ 'et', 'ou', 'mais', 'donc', 'car', 'ni', 'or',
+ 'a', 'en', 'pour', 'par', 'sur', 'avec', 'sans', 'dans', 'sous',
+ 'je', 'tu', 'il', 'elle', 'on', 'nous', 'vous', 'ils', 'elles',
+ 'mon', 'ma', 'mes', 'ton', 'ta', 'tes', 'son', 'sa', 'ses',
+ 'notre', 'nos', 'votre', 'vos', 'leur', 'leurs',
+ 'ce', 'cet', 'cette', 'ces', 'qui', 'que', 'quoi', 'dont',
+ 'est', 'sont', 'etre', 'ai', 'as', 'avoir',
+ 'pas', 'plus', 'moins', 'tres', 'aussi', 'bien', 'tout', 'tous',
+ 'me', 'te', 'se', 'lui', 'leur', 'y',
+])
+
+function tokenize(text: string): Set {
+ if (!text) return new Set()
+ const tokens = text
+ .toLowerCase()
+ .replace(/[.,;:!?()'"\-/]/g, ' ')
+ .split(/\s+/)
+ .filter((t) => t.length >= 3 && !STOP_WORDS_FR.has(t))
+ return new Set(tokens)
+}
+
+function jaccard(a: Set, b: Set): number {
+ if (a.size === 0 || b.size === 0) return 0
+ let inter = 0
+ for (const x of a) if (b.has(x)) inter++
+ const union = a.size + b.size - inter
+ return union === 0 ? 0 : inter / union
+}
+
+function score(textA: string, hashtagsA: string[], textB: string, hashtagsB: string[]): number {
+ const tagsA = new Set(hashtagsA.map((h) => h.toLowerCase()))
+ const tagsB = new Set(hashtagsB.map((h) => h.toLowerCase()))
+
+ if (tagsA.size > 0 && tagsB.size > 0) {
+ return jaccard(tagsA, tagsB)
+ }
+ return jaccard(tokenize(textA), tokenize(textB))
+}
+
+// scoreDirect tokenise TOUJOURS les textes, ignore les hashtags
+// Utilise pour matchSolution : besoin vs offre doivent etre compares par leur contenu reel
+function scoreDirect(textA: string, textB: string): number {
+ return jaccard(tokenize(textA), tokenize(textB))
+}
+
+export function matchSolution(fiches: CodevFiche[], threshold = 0.18): CodevMatch[] {
+ const matches: CodevMatch[] = []
+ for (const a of fiches) {
+ for (const b of fiches) {
+ if (a.id === b.id) continue
+ // Solution : on compare le TEXTE besoin de A avec le TEXTE offre de B
+ // On ignore les hashtags pour differencier besoin et offre
+ const s = scoreDirect(a.besoin, b.offre)
+ if (s >= threshold) {
+ matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'solution' })
+ }
+ }
+ }
+ return matches
+}
+
+export function matchAlliance(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] {
+ const matches: CodevMatch[] = []
+ for (let i = 0; i < fiches.length; i++) {
+ for (let j = i + 1; j < fiches.length; j++) {
+ const a = fiches[i], b = fiches[j]
+ // Alliance : besoins similaires — on compare hashtags si presents, sinon textes
+ const s = score(a.besoin, a.hashtags, b.besoin, b.hashtags)
+ if (s >= threshold) {
+ matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'alliance' })
+ }
+ }
+ }
+ return matches
+}
+
+export function matchSurprise(fiches: CodevFiche[], threshold = 0.25): CodevMatch[] {
+ const matches: CodevMatch[] = []
+ for (let i = 0; i < fiches.length; i++) {
+ for (let j = i + 1; j < fiches.length; j++) {
+ const a = fiches[i], b = fiches[j]
+ // Surprise : offres similaires
+ const s = score(a.offre, a.hashtags, b.offre, b.hashtags)
+ if (s >= threshold) {
+ matches.push({ fromId: a.id, toId: b.id, score: s, mode: 'surprise' })
+ }
+ }
+ }
+ return matches
+}
+
+export function computeMatches(
+ fiches: CodevFiche[],
+ mode: 'solution' | 'alliance' | 'surprise',
+ threshold?: number,
+): CodevMatch[] {
+ switch (mode) {
+ case 'solution': return matchSolution(fiches, threshold)
+ case 'alliance': return matchAlliance(fiches, threshold)
+ case 'surprise': return matchSurprise(fiches, threshold)
+ }
+}