feat(irpf): couche data open source — Eurostat + INSEE build-time
- eurostat.ts : client JSON-stat 2.0, fetch multi-pays, parser flat index - insee.ts : client BDM SDMX-JSON (prêt pour clé API) - indicators-config.ts : 32 indicateurs sur 14 thèmes (finances, emploi, santé, éducation, cohésion, logement, énergie, numérique, agriculture) avec mapping Eurostat dataset + filtres validés - fetch-indicators.ts : script pré-build → src/data/live/indicators.json - fetch-indicators integration : Astro hook astro:build:start (cache 6h) - KpiLive.astro : composant avec badge live, comparaison UE, code couleur - donnees/index.astro : tableau de bord France avec 7 groupes + tableau comparatif France-Allemagne-Espagne-Italie-Suède-Pays-Bas-UE27 - package.json : scripts fetch-data + fetch-and-build 18 indicateurs Eurostat avec valeur FR correcte (Eurostat) au premier run. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
28b406c3f3
commit
a35c43661b
11 changed files with 1497 additions and 14 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -22,3 +22,6 @@ pnpm-debug.log*
|
||||||
|
|
||||||
# jetbrains setting folder
|
# jetbrains setting folder
|
||||||
.idea/
|
.idea/
|
||||||
|
|
||||||
|
# Données live générées en build-time (Eurostat + INSEE)
|
||||||
|
src/data/live/indicators.json
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,13 @@ import { defineConfig } from 'astro/config';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import mdx from '@astrojs/mdx';
|
import mdx from '@astrojs/mdx';
|
||||||
import sitemap from '@astrojs/sitemap';
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
import { fetchIndicatorsIntegration } from './src/integrations/fetch-indicators.ts';
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: 'https://france.reverdin.eu',
|
site: 'https://france.reverdin.eu',
|
||||||
integrations: [
|
integrations: [
|
||||||
|
fetchIndicatorsIntegration(),
|
||||||
mdx(),
|
mdx(),
|
||||||
sitemap(),
|
sitemap(),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"fetch-data": "npx tsx src/scripts/fetch-indicators.ts",
|
||||||
|
"fetch-and-build": "npm run fetch-data && npm run build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.9",
|
"@astrojs/check": "^0.9.9",
|
||||||
|
|
@ -19,6 +21,7 @@
|
||||||
"astro": "^6.4.4",
|
"astro": "^6.4.4",
|
||||||
"pagefind": "^1.5.2",
|
"pagefind": "^1.5.2",
|
||||||
"tailwindcss": "^4.3.0",
|
"tailwindcss": "^4.3.0",
|
||||||
|
"tsx": "^4.19.4",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
309
pnpm-lock.yaml
generated
309
pnpm-lock.yaml
generated
|
|
@ -13,22 +13,25 @@ importers:
|
||||||
version: 0.9.9(prettier@3.8.3)(typescript@6.0.3)
|
version: 0.9.9(prettier@3.8.3)(typescript@6.0.3)
|
||||||
'@astrojs/mdx':
|
'@astrojs/mdx':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2(astro@6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(yaml@2.9.0))
|
version: 6.0.2(astro@6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(tsx@4.22.4)(yaml@2.9.0))
|
||||||
'@astrojs/sitemap':
|
'@astrojs/sitemap':
|
||||||
specifier: ^3.7.3
|
specifier: ^3.7.3
|
||||||
version: 3.7.3
|
version: 3.7.3
|
||||||
'@tailwindcss/vite':
|
'@tailwindcss/vite':
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.3.0(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))
|
version: 4.3.0(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))
|
||||||
astro:
|
astro:
|
||||||
specifier: ^6.4.4
|
specifier: ^6.4.4
|
||||||
version: 6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(yaml@2.9.0)
|
version: 6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(tsx@4.22.4)(yaml@2.9.0)
|
||||||
pagefind:
|
pagefind:
|
||||||
specifier: ^1.5.2
|
specifier: ^1.5.2
|
||||||
version: 1.5.2
|
version: 1.5.2
|
||||||
tailwindcss:
|
tailwindcss:
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.19.4
|
||||||
|
version: 4.22.4
|
||||||
typescript:
|
typescript:
|
||||||
specifier: ^6.0.3
|
specifier: ^6.0.3
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
|
|
@ -148,156 +151,312 @@ packages:
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [aix]
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
'@esbuild/android-arm64@0.27.7':
|
'@esbuild/android-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
|
resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/android-arm@0.27.7':
|
'@esbuild/android-arm@0.27.7':
|
||||||
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
|
resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.28.0':
|
||||||
|
resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/android-x64@0.27.7':
|
'@esbuild/android-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
|
resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
'@esbuild/darwin-arm64@0.27.7':
|
'@esbuild/darwin-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
|
resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
'@esbuild/darwin-x64@0.27.7':
|
'@esbuild/darwin-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
|
resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
'@esbuild/freebsd-arm64@0.27.7':
|
'@esbuild/freebsd-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
|
resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
'@esbuild/freebsd-x64@0.27.7':
|
'@esbuild/freebsd-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
|
resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
'@esbuild/linux-arm64@0.27.7':
|
'@esbuild/linux-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
|
resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-arm@0.27.7':
|
'@esbuild/linux-arm@0.27.7':
|
||||||
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
|
resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.28.0':
|
||||||
|
resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-ia32@0.27.7':
|
'@esbuild/linux-ia32@0.27.7':
|
||||||
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
|
resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.28.0':
|
||||||
|
resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.27.7':
|
'@esbuild/linux-loong64@0.27.7':
|
||||||
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
|
resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-mips64el@0.27.7':
|
'@esbuild/linux-mips64el@0.27.7':
|
||||||
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
|
resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [mips64el]
|
cpu: [mips64el]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.28.0':
|
||||||
|
resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-ppc64@0.27.7':
|
'@esbuild/linux-ppc64@0.27.7':
|
||||||
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
|
resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-riscv64@0.27.7':
|
'@esbuild/linux-riscv64@0.27.7':
|
||||||
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
|
resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-s390x@0.27.7':
|
'@esbuild/linux-s390x@0.27.7':
|
||||||
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
|
resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.28.0':
|
||||||
|
resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/linux-x64@0.27.7':
|
'@esbuild/linux-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
|
resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
'@esbuild/netbsd-arm64@0.27.7':
|
'@esbuild/netbsd-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
|
resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [netbsd]
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
'@esbuild/netbsd-x64@0.27.7':
|
'@esbuild/netbsd-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
|
resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [netbsd]
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
'@esbuild/openbsd-arm64@0.27.7':
|
'@esbuild/openbsd-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
|
resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [openbsd]
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
'@esbuild/openbsd-x64@0.27.7':
|
'@esbuild/openbsd-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
|
resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [openbsd]
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
'@esbuild/openharmony-arm64@0.27.7':
|
'@esbuild/openharmony-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
|
resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [openharmony]
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
'@esbuild/sunos-x64@0.27.7':
|
'@esbuild/sunos-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
|
resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [sunos]
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
'@esbuild/win32-arm64@0.27.7':
|
'@esbuild/win32-arm64@0.27.7':
|
||||||
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
|
resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@esbuild/win32-ia32@0.27.7':
|
'@esbuild/win32-ia32@0.27.7':
|
||||||
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
|
resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.28.0':
|
||||||
|
resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@esbuild/win32-x64@0.27.7':
|
'@esbuild/win32-x64@0.27.7':
|
||||||
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
|
resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.28.0':
|
||||||
|
resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@img/colour@1.1.0':
|
'@img/colour@1.1.0':
|
||||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -1080,6 +1239,11 @@ packages:
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
esbuild@0.28.0:
|
||||||
|
resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
escalade@3.2.0:
|
escalade@3.2.0:
|
||||||
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -1852,6 +2016,11 @@ packages:
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
tsx@4.22.4:
|
||||||
|
resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
typesafe-path@0.2.2:
|
typesafe-path@0.2.2:
|
||||||
resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==}
|
resolution: {integrity: sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA==}
|
||||||
|
|
||||||
|
|
@ -2261,13 +2430,13 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@astrojs/mdx@6.0.2(astro@6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(yaml@2.9.0))':
|
'@astrojs/mdx@6.0.2(astro@6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(tsx@4.22.4)(yaml@2.9.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/internal-helpers': 0.10.0
|
'@astrojs/internal-helpers': 0.10.0
|
||||||
'@astrojs/markdown-remark': 7.2.0
|
'@astrojs/markdown-remark': 7.2.0
|
||||||
'@mdx-js/mdx': 3.1.1
|
'@mdx-js/mdx': 3.1.1
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
astro: 6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(yaml@2.9.0)
|
astro: 6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(tsx@4.22.4)(yaml@2.9.0)
|
||||||
es-module-lexer: 2.1.0
|
es-module-lexer: 2.1.0
|
||||||
estree-util-visit: 2.0.0
|
estree-util-visit: 2.0.0
|
||||||
hast-util-to-html: 9.0.5
|
hast-util-to-html: 9.0.5
|
||||||
|
|
@ -2363,81 +2532,159 @@ snapshots:
|
||||||
'@esbuild/aix-ppc64@0.27.7':
|
'@esbuild/aix-ppc64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm64@0.27.7':
|
'@esbuild/android-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-arm@0.27.7':
|
'@esbuild/android-arm@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/android-x64@0.27.7':
|
'@esbuild/android-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/darwin-arm64@0.27.7':
|
'@esbuild/darwin-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/darwin-x64@0.27.7':
|
'@esbuild/darwin-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/freebsd-arm64@0.27.7':
|
'@esbuild/freebsd-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/freebsd-x64@0.27.7':
|
'@esbuild/freebsd-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-arm64@0.27.7':
|
'@esbuild/linux-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-arm@0.27.7':
|
'@esbuild/linux-arm@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-ia32@0.27.7':
|
'@esbuild/linux-ia32@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-loong64@0.27.7':
|
'@esbuild/linux-loong64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-mips64el@0.27.7':
|
'@esbuild/linux-mips64el@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-ppc64@0.27.7':
|
'@esbuild/linux-ppc64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-riscv64@0.27.7':
|
'@esbuild/linux-riscv64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-s390x@0.27.7':
|
'@esbuild/linux-s390x@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/linux-x64@0.27.7':
|
'@esbuild/linux-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/netbsd-arm64@0.27.7':
|
'@esbuild/netbsd-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/netbsd-x64@0.27.7':
|
'@esbuild/netbsd-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/openbsd-arm64@0.27.7':
|
'@esbuild/openbsd-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/openbsd-x64@0.27.7':
|
'@esbuild/openbsd-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/openharmony-arm64@0.27.7':
|
'@esbuild/openharmony-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/sunos-x64@0.27.7':
|
'@esbuild/sunos-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-arm64@0.27.7':
|
'@esbuild/win32-arm64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-ia32@0.27.7':
|
'@esbuild/win32-ia32@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/win32-x64@0.27.7':
|
'@esbuild/win32-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.28.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@img/colour@1.1.0':
|
'@img/colour@1.1.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -2791,12 +3038,12 @@ snapshots:
|
||||||
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
|
'@tailwindcss/oxide-win32-arm64-msvc': 4.3.0
|
||||||
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
|
'@tailwindcss/oxide-win32-x64-msvc': 4.3.0
|
||||||
|
|
||||||
'@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))':
|
'@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tailwindcss/node': 4.3.0
|
'@tailwindcss/node': 4.3.0
|
||||||
'@tailwindcss/oxide': 4.3.0
|
'@tailwindcss/oxide': 4.3.0
|
||||||
tailwindcss: 4.3.0
|
tailwindcss: 4.3.0
|
||||||
vite: 7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)
|
vite: 7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||||
|
|
||||||
'@types/debug@4.1.13':
|
'@types/debug@4.1.13':
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -2926,7 +3173,7 @@ snapshots:
|
||||||
|
|
||||||
astring@1.9.0: {}
|
astring@1.9.0: {}
|
||||||
|
|
||||||
astro@6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(yaml@2.9.0):
|
astro@6.4.4(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.61.1)(tsx@4.22.4)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/compiler': 4.0.0
|
'@astrojs/compiler': 4.0.0
|
||||||
'@astrojs/internal-helpers': 0.10.0
|
'@astrojs/internal-helpers': 0.10.0
|
||||||
|
|
@ -2978,8 +3225,8 @@ snapshots:
|
||||||
unist-util-visit: 5.1.0
|
unist-util-visit: 5.1.0
|
||||||
unstorage: 1.17.5
|
unstorage: 1.17.5
|
||||||
vfile: 6.0.3
|
vfile: 6.0.3
|
||||||
vite: 7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)
|
vite: 7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||||
vitefu: 1.1.3(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))
|
vitefu: 1.1.3(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0))
|
||||||
xxhash-wasm: 1.1.0
|
xxhash-wasm: 1.1.0
|
||||||
yargs-parser: 22.0.0
|
yargs-parser: 22.0.0
|
||||||
zod: 4.4.3
|
zod: 4.4.3
|
||||||
|
|
@ -3204,6 +3451,35 @@ snapshots:
|
||||||
'@esbuild/win32-ia32': 0.27.7
|
'@esbuild/win32-ia32': 0.27.7
|
||||||
'@esbuild/win32-x64': 0.27.7
|
'@esbuild/win32-x64': 0.27.7
|
||||||
|
|
||||||
|
esbuild@0.28.0:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.28.0
|
||||||
|
'@esbuild/android-arm': 0.28.0
|
||||||
|
'@esbuild/android-arm64': 0.28.0
|
||||||
|
'@esbuild/android-x64': 0.28.0
|
||||||
|
'@esbuild/darwin-arm64': 0.28.0
|
||||||
|
'@esbuild/darwin-x64': 0.28.0
|
||||||
|
'@esbuild/freebsd-arm64': 0.28.0
|
||||||
|
'@esbuild/freebsd-x64': 0.28.0
|
||||||
|
'@esbuild/linux-arm': 0.28.0
|
||||||
|
'@esbuild/linux-arm64': 0.28.0
|
||||||
|
'@esbuild/linux-ia32': 0.28.0
|
||||||
|
'@esbuild/linux-loong64': 0.28.0
|
||||||
|
'@esbuild/linux-mips64el': 0.28.0
|
||||||
|
'@esbuild/linux-ppc64': 0.28.0
|
||||||
|
'@esbuild/linux-riscv64': 0.28.0
|
||||||
|
'@esbuild/linux-s390x': 0.28.0
|
||||||
|
'@esbuild/linux-x64': 0.28.0
|
||||||
|
'@esbuild/netbsd-arm64': 0.28.0
|
||||||
|
'@esbuild/netbsd-x64': 0.28.0
|
||||||
|
'@esbuild/openbsd-arm64': 0.28.0
|
||||||
|
'@esbuild/openbsd-x64': 0.28.0
|
||||||
|
'@esbuild/openharmony-arm64': 0.28.0
|
||||||
|
'@esbuild/sunos-x64': 0.28.0
|
||||||
|
'@esbuild/win32-arm64': 0.28.0
|
||||||
|
'@esbuild/win32-ia32': 0.28.0
|
||||||
|
'@esbuild/win32-x64': 0.28.0
|
||||||
|
|
||||||
escalade@3.2.0: {}
|
escalade@3.2.0: {}
|
||||||
|
|
||||||
escape-string-regexp@5.0.0: {}
|
escape-string-regexp@5.0.0: {}
|
||||||
|
|
@ -4406,6 +4682,12 @@ snapshots:
|
||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
tsx@4.22.4:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.28.0
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
typesafe-path@0.2.2: {}
|
typesafe-path@0.2.2: {}
|
||||||
|
|
||||||
typescript-auto-import-cache@0.3.6:
|
typescript-auto-import-cache@0.3.6:
|
||||||
|
|
@ -4510,7 +4792,7 @@ snapshots:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
vfile-message: 4.0.3
|
vfile-message: 4.0.3
|
||||||
|
|
||||||
vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0):
|
vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
fdir: 6.5.0(picomatch@4.0.4)
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
|
|
@ -4523,11 +4805,12 @@ snapshots:
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
|
tsx: 4.22.4
|
||||||
yaml: 2.9.0
|
yaml: 2.9.0
|
||||||
|
|
||||||
vitefu@1.1.3(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)):
|
vitefu@1.1.3(vite@7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)
|
vite: 7.3.5(@types/node@24.13.0)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.22.4)(yaml@2.9.0)
|
||||||
|
|
||||||
volar-service-css@0.0.70(@volar/language-service@2.4.28):
|
volar-service-css@0.0.70(@volar/language-service@2.4.28):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
90
src/components/KpiLive.astro
Normal file
90
src/components/KpiLive.astro
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
---
|
||||||
|
/**
|
||||||
|
* KpiLive — Indicateur open data avec badge "données en direct"
|
||||||
|
* Affiche valeur France + mini comparatif pays si demandé.
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PaysValue {
|
||||||
|
value: number | null;
|
||||||
|
year: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id?: string;
|
||||||
|
label: string;
|
||||||
|
valeur: number | null;
|
||||||
|
annee: string;
|
||||||
|
unite: string;
|
||||||
|
sens_positif?: 'hausse' | 'baisse';
|
||||||
|
pays?: Record<string, PaysValue>;
|
||||||
|
note?: string;
|
||||||
|
compact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { label, valeur, annee, unite, sens_positif, pays, note, compact = false } = Astro.props;
|
||||||
|
|
||||||
|
// Tendance : comparer FR à EU27 si dispo
|
||||||
|
const eu27 = pays?.['EU27_2020']?.value ?? null;
|
||||||
|
let tendance: 'meilleur' | 'pire' | 'neutre' = 'neutre';
|
||||||
|
if (valeur !== null && eu27 !== null) {
|
||||||
|
const diff = valeur - eu27;
|
||||||
|
if (sens_positif === 'hausse') tendance = diff > 0.5 ? 'meilleur' : diff < -0.5 ? 'pire' : 'neutre';
|
||||||
|
if (sens_positif === 'baisse') tendance = diff < -0.5 ? 'meilleur' : diff > 0.5 ? 'pire' : 'neutre';
|
||||||
|
}
|
||||||
|
|
||||||
|
const valeurFormatted = valeur !== null
|
||||||
|
? (Math.abs(valeur) >= 100 ? Math.round(valeur).toLocaleString('fr-FR') : valeur.toFixed(1).replace('.0', ''))
|
||||||
|
: 'N/D';
|
||||||
|
|
||||||
|
const tenantColor = tendance === 'meilleur' ? '#16a34a' : tendance === 'pire' ? '#dc2626' : '#6b7280';
|
||||||
|
const tenantBg = tendance === 'meilleur' ? '#f0fdf4' : tendance === 'pire' ? '#fef2f2' : '#f9fafb';
|
||||||
|
|
||||||
|
// 3 pays clés pour comparaison rapide
|
||||||
|
const paysKeys = ['DE', 'SE', 'EU27_2020'].filter(k => pays?.[k]?.value !== null);
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={`kpi-live ${compact ? 'kpi-live--compact' : ''}`} style={`background: ${tenantBg}; border: 1px solid ${tendance === 'neutre' ? '#e5e7eb' : tenantColor + '40'}; border-radius: 0.5rem; padding: ${compact ? '0.75rem 1rem' : '1rem 1.25rem'}; position: relative; overflow: hidden;`}>
|
||||||
|
|
||||||
|
<!-- Badge live -->
|
||||||
|
<div style="position: absolute; top: 0.5rem; right: 0.5rem; display: flex; align-items: center; gap: 0.3rem;">
|
||||||
|
<span style="width: 6px; height: 6px; background: #22c55e; border-radius: 50%; animation: pulse 2s infinite;"></span>
|
||||||
|
<span style="font-size: 0.6rem; color: #6b7280; letter-spacing: 0.05em; font-weight: 500;">Eurostat {annee}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Valeur principale -->
|
||||||
|
<div style="display: flex; align-items: baseline; gap: 0.35rem; margin-bottom: 0.15rem;">
|
||||||
|
<span style={`font-size: ${compact ? '1.35rem' : '1.6rem'}; font-weight: 700; color: ${valeur !== null ? '#0f172a' : '#9ca3af'}; line-height: 1;`}>{valeurFormatted}</span>
|
||||||
|
{valeur !== null && <span style="font-size: 0.75rem; color: #6b7280; font-weight: 400;">{unite}</span>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Label -->
|
||||||
|
<div style={`font-size: ${compact ? '0.75rem' : '0.8rem'}; color: #374151; font-weight: 500; line-height: 1.3; margin-bottom: ${!compact && paysKeys.length > 0 ? '0.75rem' : '0'};`}>{label}</div>
|
||||||
|
|
||||||
|
<!-- Comparaison rapide -->
|
||||||
|
{!compact && paysKeys.length > 0 && valeur !== null && (
|
||||||
|
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
|
||||||
|
{paysKeys.map(code => {
|
||||||
|
const pVal = pays?.[code];
|
||||||
|
if (!pVal || pVal.value === null) return null;
|
||||||
|
const isEU = code === 'EU27_2020';
|
||||||
|
const v = pVal.value;
|
||||||
|
const formatted = Math.abs(v) >= 100 ? Math.round(v).toLocaleString('fr-FR') : v.toFixed(1).replace('.0', '');
|
||||||
|
return (
|
||||||
|
<span style="font-size: 0.65rem; color: #6b7280; background: white; border: 1px solid #e5e7eb; padding: 0.1rem 0.4rem; border-radius: 0.25rem;">
|
||||||
|
{isEU ? 'UE' : code} {formatted} {unite}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{note && <div style="font-size: 0.65rem; color: #9ca3af; margin-top: 0.5rem; font-style: italic;">{note}</div>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
50
src/integrations/fetch-indicators.ts
Normal file
50
src/integrations/fetch-indicators.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* Intégration Astro — Fetch automatique des indicateurs open data en pré-build
|
||||||
|
* S'exécute via le hook astro:build:start (avant la génération des pages)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { AstroIntegration } from 'astro';
|
||||||
|
import { existsSync, statSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const INDICATORS_FILE = new URL('../../src/data/live/indicators.json', import.meta.url).pathname;
|
||||||
|
const MAX_AGE_HOURS = 6; // Rafraîchir si plus vieux que 6h
|
||||||
|
|
||||||
|
function isStale(): boolean {
|
||||||
|
if (!existsSync(INDICATORS_FILE)) return true;
|
||||||
|
const stat = statSync(INDICATORS_FILE);
|
||||||
|
const ageMs = Date.now() - stat.mtimeMs;
|
||||||
|
return ageMs > MAX_AGE_HOURS * 3600 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fetchIndicatorsIntegration(): AstroIntegration {
|
||||||
|
return {
|
||||||
|
name: 'irpf:fetch-indicators',
|
||||||
|
hooks: {
|
||||||
|
'astro:build:start': async ({ logger }) => {
|
||||||
|
if (!isStale()) {
|
||||||
|
logger.info('Indicateurs open data — cache frais (< 6h), skip fetch');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info('Indicateurs open data — fetch Eurostat + INSEE...');
|
||||||
|
try {
|
||||||
|
// Import dynamique pour éviter les problèmes de résolution au démarrage
|
||||||
|
const { spawnSync } = await import('child_process');
|
||||||
|
const scriptPath = new URL('../scripts/fetch-indicators.ts', import.meta.url).pathname;
|
||||||
|
const result = spawnSync('npx', ['tsx', scriptPath], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
encoding: 'utf-8',
|
||||||
|
timeout: 120_000, // 2 min max
|
||||||
|
});
|
||||||
|
if (result.status !== 0) {
|
||||||
|
logger.warn('Fetch indicateurs échoué — build continue avec données existantes');
|
||||||
|
} else {
|
||||||
|
logger.info('Indicateurs mis à jour ✓');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.warn(`Fetch indicateurs: ${(e as Error).message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
136
src/lib/eurostat.ts
Normal file
136
src/lib/eurostat.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
/**
|
||||||
|
* Client Eurostat — API JSON-stat 2.0
|
||||||
|
* CORS ouvert, pas d'authentification requise.
|
||||||
|
* Doc: https://wikis.ec.europa.eu/display/EUROSTATHELP/API+Statistics+-+data+query
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASE = 'https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data';
|
||||||
|
|
||||||
|
export interface EurostatValue {
|
||||||
|
value: number | null;
|
||||||
|
year: string;
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GeoValues = Record<string, EurostatValue>; // { FR: {...}, DE: {...}, ... }
|
||||||
|
|
||||||
|
interface JSONStat {
|
||||||
|
id: string[];
|
||||||
|
size: number[];
|
||||||
|
dimension: Record<string, {
|
||||||
|
label: string;
|
||||||
|
category: {
|
||||||
|
index: Record<string, number>;
|
||||||
|
label: Record<string, string>;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
value: Record<string, number> | number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse une réponse JSON-stat et extrait la dernière valeur pour chaque pays.
|
||||||
|
* Supporte les requêtes multi-géographies.
|
||||||
|
*/
|
||||||
|
function parseJSONStat(data: JSONStat, geos: string[]): GeoValues {
|
||||||
|
const result: GeoValues = {};
|
||||||
|
|
||||||
|
const timeIdx = data.id.indexOf('time');
|
||||||
|
const geoIdx = data.id.indexOf('geo');
|
||||||
|
if (timeIdx === -1 || geoIdx === -1) return result;
|
||||||
|
|
||||||
|
const timeCat = data.dimension['time']?.category;
|
||||||
|
const geoCat = data.dimension['geo']?.category;
|
||||||
|
if (!timeCat || !geoCat) return result;
|
||||||
|
|
||||||
|
// Trouver l'année la plus récente avec une valeur
|
||||||
|
const timeEntries = Object.entries(timeCat.index).sort((a, b) => b[1] - a[1]); // décroissant
|
||||||
|
|
||||||
|
// Calculer les strides pour l'indexation flat
|
||||||
|
const strides: number[] = new Array(data.id.length).fill(1);
|
||||||
|
for (let i = data.id.length - 2; i >= 0; i--) {
|
||||||
|
strides[i] = strides[i + 1] * data.size[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
const values = Array.isArray(data.value)
|
||||||
|
? data.value
|
||||||
|
: (idx: number) => (data.value as Record<string, number>)[String(idx)];
|
||||||
|
|
||||||
|
const getValue = Array.isArray(data.value)
|
||||||
|
? (idx: number) => (data.value as number[])[idx]
|
||||||
|
: (idx: number) => (data.value as Record<string, number>)[String(idx)];
|
||||||
|
|
||||||
|
for (const geo of geos) {
|
||||||
|
const geoPos = geoCat.index[geo];
|
||||||
|
if (geoPos === undefined) continue;
|
||||||
|
|
||||||
|
// Chercher la dernière année non-nulle
|
||||||
|
let found = false;
|
||||||
|
for (const [year, timePos] of timeEntries) {
|
||||||
|
// Calcul de l'index flat : somme de (position * stride) pour chaque dim
|
||||||
|
// Les autres dims ont taille 1 donc position 0
|
||||||
|
const flatIdx = geoPos * strides[geoIdx] + timePos * strides[timeIdx];
|
||||||
|
const val = getValue(flatIdx);
|
||||||
|
if (val !== null && val !== undefined && !isNaN(val)) {
|
||||||
|
result[geo] = { value: val, year };
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) result[geo] = { value: null, year: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch un indicateur Eurostat pour plusieurs pays.
|
||||||
|
* Retourne les dernières valeurs disponibles par pays.
|
||||||
|
*/
|
||||||
|
export async function fetchIndicateur(
|
||||||
|
dataset: string,
|
||||||
|
filters: Record<string, string | string[]>,
|
||||||
|
geos: string[] = ['FR', 'DE', 'ES', 'IT', 'SE', 'NL', 'EU27_2020'],
|
||||||
|
): Promise<GeoValues> {
|
||||||
|
try {
|
||||||
|
const url = new URL(`${BASE}/${dataset}`);
|
||||||
|
url.searchParams.set('lang', 'en');
|
||||||
|
url.searchParams.set('format', 'JSON');
|
||||||
|
|
||||||
|
for (const geo of geos) url.searchParams.append('geo', geo);
|
||||||
|
|
||||||
|
for (const [key, val] of Object.entries(filters)) {
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
for (const v of val) url.searchParams.append(key, v);
|
||||||
|
} else {
|
||||||
|
url.searchParams.set(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url.toString(), {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
signal: AbortSignal.timeout(15_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn(`[eurostat] ${dataset} → HTTP ${res.status}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: JSONStat = await res.json();
|
||||||
|
return parseJSONStat(data, geos);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[eurostat] ${dataset} → ${(e as Error).message}`);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch la même série pour N pays — alias plus lisible.
|
||||||
|
*/
|
||||||
|
export async function fetchMultiPays(
|
||||||
|
dataset: string,
|
||||||
|
filters: Record<string, string>,
|
||||||
|
pays: string[],
|
||||||
|
): Promise<GeoValues> {
|
||||||
|
return fetchIndicateur(dataset, filters, pays);
|
||||||
|
}
|
||||||
455
src/lib/indicators-config.ts
Normal file
455
src/lib/indicators-config.ts
Normal file
|
|
@ -0,0 +1,455 @@
|
||||||
|
/**
|
||||||
|
* Catalogue complet des indicateurs IRPF
|
||||||
|
* Sources : Eurostat (principal) + INSEE BDM (national)
|
||||||
|
*
|
||||||
|
* Chaque indicateur est mappé à un thème IRPF et à sa source de données.
|
||||||
|
* Utilisé par le script fetch-indicators.ts pour générer indicators.json.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const PAYS_COMPARES = ['FR', 'DE', 'ES', 'IT', 'SE', 'NL', 'EU27_2020'] as const;
|
||||||
|
|
||||||
|
export type PaysCode = typeof PAYS_COMPARES[number];
|
||||||
|
|
||||||
|
export const PAYS_LABEL: Record<string, string> = {
|
||||||
|
FR: 'France',
|
||||||
|
DE: 'Allemagne',
|
||||||
|
ES: 'Espagne',
|
||||||
|
IT: 'Italie',
|
||||||
|
SE: 'Suède',
|
||||||
|
NL: 'Pays-Bas',
|
||||||
|
EU27_2020: 'Moyenne UE27',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ─── TYPES ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface IndicateurConfig {
|
||||||
|
id: string; // identifiant unique IRPF (ex: "deficit_pib")
|
||||||
|
label: string; // libellé court affiché
|
||||||
|
description: string; // description pour tooltip
|
||||||
|
unite: string; // "% PIB", "années", "pour 1 000 hab.", etc.
|
||||||
|
theme: string | string[]; // slug(s) du/des thème(s) IRPF
|
||||||
|
source: 'eurostat' | 'insee';
|
||||||
|
// Eurostat
|
||||||
|
dataset?: string;
|
||||||
|
filters?: Record<string, string>;
|
||||||
|
// INSEE
|
||||||
|
serie_id?: string;
|
||||||
|
// Méta
|
||||||
|
sens_positif?: 'hausse' | 'baisse'; // hausse = bon signe pour la France
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── INDICATEURS EUROSTAT ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const INDICATEURS: IndicateurConfig[] = [
|
||||||
|
|
||||||
|
// ── Économie & Finances publiques ─────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'deficit_pib',
|
||||||
|
label: 'Déficit public',
|
||||||
|
description: 'Capacité (+) ou besoin (−) de financement des administrations publiques',
|
||||||
|
unite: '% PIB',
|
||||||
|
theme: 'economie-finances',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'gov_10a_main',
|
||||||
|
filters: { na_item: 'B9', sector: 'S13', unit: 'PC_GDP' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
note: 'Valeur négative = déficit. Plafond Maastricht : −3 % PIB.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dette_pib',
|
||||||
|
label: 'Dette publique',
|
||||||
|
description: 'Dette publique brute consolidée (critère de Maastricht)',
|
||||||
|
unite: '% PIB',
|
||||||
|
theme: 'economie-finances',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'gov_10dd_edpt1',
|
||||||
|
filters: { na_item: 'GD', sector: 'S13', unit: 'PC_GDP' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
note: 'Plafond Maastricht : 60 % PIB.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'depenses_publiques_pib',
|
||||||
|
label: 'Dépenses publiques',
|
||||||
|
description: 'Total des dépenses des administrations publiques',
|
||||||
|
unite: '% PIB',
|
||||||
|
theme: 'economie-finances',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'gov_10a_main',
|
||||||
|
filters: { na_item: 'TE', sector: 'S13', unit: 'PC_GDP' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prelevements_obligatoires',
|
||||||
|
label: 'Prélèvements obligatoires',
|
||||||
|
description: 'Total des recettes fiscales et cotisations sociales',
|
||||||
|
unite: '% PIB',
|
||||||
|
theme: 'economie-finances',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'gov_10a_main',
|
||||||
|
filters: { na_item: 'TR', sector: 'S13', unit: 'PC_GDP' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'croissance_pib',
|
||||||
|
label: 'Croissance du PIB',
|
||||||
|
description: 'Taux de croissance réel du PIB en volume',
|
||||||
|
unite: '% / an',
|
||||||
|
theme: ['economie-finances', 'emploi-travail'],
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'nama_10_gdp',
|
||||||
|
filters: { na_item: 'B1GQ', unit: 'CLV_PCH_PRE' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'depenses_sociales_pib',
|
||||||
|
label: 'Dépenses sociales',
|
||||||
|
description: 'Dépenses de protection sociale (pensions, santé, chômage, famille)',
|
||||||
|
unite: '% PIB',
|
||||||
|
theme: ['economie-finances', 'cohesion-sociale'],
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'tps00098',
|
||||||
|
filters: { unit: 'PC_GDP' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Emploi & Travail ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'taux_chomage',
|
||||||
|
label: 'Chômage longue durée',
|
||||||
|
description: 'Chômeurs depuis ≥ 12 mois en % de la population active',
|
||||||
|
unite: '%',
|
||||||
|
theme: 'emploi-travail',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'sdg_08_40',
|
||||||
|
filters: { sex: 'T' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
note: 'Indicateur SDG 8 (chômage longue durée). Taux total BIT France ≈ 7,3 % (INSEE 2025).',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'taux_chomage_jeunes',
|
||||||
|
label: 'Chômage des jeunes',
|
||||||
|
description: 'Taux de chômage des 15-24 ans',
|
||||||
|
unite: '%',
|
||||||
|
theme: 'emploi-travail',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'une_rt_a',
|
||||||
|
filters: { sex: 'T', age: 'Y15-24', unit: 'PC_ACT' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'taux_emploi',
|
||||||
|
label: "Taux d'emploi",
|
||||||
|
description: "Part de la population de 20-64 ans en emploi",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'emploi-travail',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'sdg_08_30',
|
||||||
|
filters: { sex: 'T' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'taux_emploi_seniors',
|
||||||
|
label: 'Emploi seniors (55-64 ans)',
|
||||||
|
description: "Part de la population de 55-64 ans en emploi",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'emploi-travail',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'sdg_08_30',
|
||||||
|
filters: { sex: 'T', age: 'Y55-64' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Santé ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'esperance_vie',
|
||||||
|
label: 'Espérance de vie à la naissance',
|
||||||
|
description: 'Espérance de vie des deux sexes combinés',
|
||||||
|
unite: 'années',
|
||||||
|
theme: 'sante',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'demo_mlexpec',
|
||||||
|
filters: { sex: 'T', age: 'Y_LT1' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'depenses_sante_pib',
|
||||||
|
label: 'Dépenses de santé',
|
||||||
|
description: 'Total des dépenses courantes de santé (publiques + privées)',
|
||||||
|
unite: '% PIB',
|
||||||
|
theme: 'sante',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'tps00207',
|
||||||
|
filters: { unit: 'PC_GDP' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
note: 'Plus élevé = système plus généreux. Attention aux effets qualité.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mortalite_traitable',
|
||||||
|
label: 'Mortalité traitable',
|
||||||
|
description: 'Décès évitables grâce aux soins médicaux (pour 100 000 hab.)',
|
||||||
|
unite: 'pour 100 000 hab.',
|
||||||
|
theme: 'sante',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'hlth_cdeath_101',
|
||||||
|
filters: { sex: 'T', unit: 'RT' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mortalite_preventable',
|
||||||
|
label: 'Mortalité prévenable',
|
||||||
|
description: 'Décès évitables par la prévention (mode de vie, facteurs de risque)',
|
||||||
|
unite: 'pour 100 000 hab.',
|
||||||
|
theme: 'sante',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'hlth_cdeath_102',
|
||||||
|
filters: { sex: 'T', unit: 'RT' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Éducation ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'depenses_education_pib',
|
||||||
|
label: "Dépenses d'éducation",
|
||||||
|
description: "Dépenses publiques en éducation (tous niveaux)",
|
||||||
|
unite: '% PIB',
|
||||||
|
theme: 'education',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'educ_uoe_fine09',
|
||||||
|
filters: { unit: 'PC_GDP', isced11: 'TOTAL' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'decrochage_scolaire',
|
||||||
|
label: 'Décrochage scolaire',
|
||||||
|
description: "Part des 18-24 ans sans diplôme du secondaire et hors formation",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'education',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'edat_lfse_14',
|
||||||
|
filters: { sex: 'T', unit: 'PC' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'diplomes_superieurs',
|
||||||
|
label: 'Diplômés de l\'enseignement supérieur',
|
||||||
|
description: "Part de la population 25-34 ans avec un diplôme du supérieur",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'education',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'edat_lfse_03',
|
||||||
|
filters: { sex: 'T', age: 'Y25-34', unit: 'PC' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Cohésion sociale & Inégalités ─────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'gini',
|
||||||
|
label: 'Coefficient de Gini',
|
||||||
|
description: "Mesure des inégalités de revenus après impôts (0=égalité parfaite, 100=inégalité totale)",
|
||||||
|
unite: 'indice',
|
||||||
|
theme: 'cohesion-sociale',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'ilc_di12',
|
||||||
|
filters: {},
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'taux_pauvrete',
|
||||||
|
label: 'Taux de pauvreté',
|
||||||
|
description: "Part de la population sous le seuil de pauvreté (60% du revenu médian)",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'cohesion-sociale',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'ilc_peps01n',
|
||||||
|
filters: { unit: 'PC', age: 'TOTAL', sex: 'T' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'taux_pauvrete_enfants',
|
||||||
|
label: 'Pauvreté des enfants',
|
||||||
|
description: "Taux de pauvreté des moins de 18 ans",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'cohesion-sociale',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'ilc_peps01n',
|
||||||
|
filters: { unit: 'PC', age: 'Y_LT18', sex: 'T' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Logement ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'indice_prix_immobilier',
|
||||||
|
label: 'Prix immobilier',
|
||||||
|
description: "Indice des prix résidentiels (base 100 = 2015)",
|
||||||
|
unite: 'indice base 100 (2015)',
|
||||||
|
theme: 'logement',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'prc_hpi_a',
|
||||||
|
filters: { purchase: 'TOTAL' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
note: 'Hausse = aggravation de l\'accès à la propriété.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'surcharge_logement',
|
||||||
|
label: 'Surcharge du coût logement',
|
||||||
|
description: "Part des ménages dont le coût du logement dépasse 40% du revenu",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'logement',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'ilc_lvho07a',
|
||||||
|
filters: { hhtyp: 'TOTAL', incgrp: 'TOTAL', unit: 'PC' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'taux_proprietaires',
|
||||||
|
label: 'Taux de propriétaires',
|
||||||
|
description: "Part de la population propriétaire de son logement",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'logement',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'ilc_lvho02',
|
||||||
|
filters: { incgrp: 'TOTAL', hhtyp: 'TOTAL', unit: 'PC' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Environnement & Énergie ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'emissions_ghg_per_capita',
|
||||||
|
label: 'Émissions GES par habitant',
|
||||||
|
description: "Émissions de gaz à effet de serre en tonnes CO2 équivalent par habitant",
|
||||||
|
unite: 't CO₂eq / hab.',
|
||||||
|
theme: 'energie-environnement',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'sdg_13_10',
|
||||||
|
filters: { unit: 'T_HAB' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'part_energies_renouvelables',
|
||||||
|
label: 'Énergies renouvelables',
|
||||||
|
description: "Part des renouvelables dans la consommation d'énergie finale",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'energie-environnement',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'nrg_ind_ren',
|
||||||
|
filters: {},
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'intensite_energetique',
|
||||||
|
label: 'Intensité énergétique',
|
||||||
|
description: "Consommation d'énergie primaire par unité de PIB",
|
||||||
|
unite: 'kgoe / k€ PIB',
|
||||||
|
theme: 'energie-environnement',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'nrg_ind_ei',
|
||||||
|
filters: { unit: 'KGOE_TEUR_PPS' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Agriculture ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'balance_agroalimentaire',
|
||||||
|
label: 'Balance agroalimentaire',
|
||||||
|
description: "Solde exportations-importations de produits agroalimentaires",
|
||||||
|
unite: 'M€',
|
||||||
|
theme: 'agriculture',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'ext_lt_intratrd',
|
||||||
|
filters: { sitc06: 'SITC0_1', flow: 'BAL', unit: 'MIO_EUR' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Numérique ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'desi_score',
|
||||||
|
label: 'Score DESI',
|
||||||
|
description: "Indice de l'économie et de la société numériques (score global)",
|
||||||
|
unite: 'score',
|
||||||
|
theme: 'numerique',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'desi_3a2_shbb',
|
||||||
|
filters: {},
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
note: 'DESI composite — voir Eurostat pour sous-indices.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'competences_numeriques',
|
||||||
|
label: 'Compétences numériques de base',
|
||||||
|
description: "Part de la population (16-74 ans) avec des compétences numériques au moins basiques",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'numerique',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'isoc_sk_dskl_i21',
|
||||||
|
filters: { unit: 'PC_IND', indic_is: 'I_DSK_AB' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Démocratie & Gouvernance ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'confiance_institutions',
|
||||||
|
label: 'Confiance dans les institutions nationales',
|
||||||
|
description: "Part des citoyens faisant confiance au gouvernement national",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'democratie',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'ilc_pw03',
|
||||||
|
filters: { indic_wb: 'TRUSTINST', unit: 'PC' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Sécurité ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'taux_homicide',
|
||||||
|
label: 'Taux d\'homicides intentionnels',
|
||||||
|
description: "Homicides intentionnels pour 100 000 habitants",
|
||||||
|
unite: 'pour 100 000 hab.',
|
||||||
|
theme: 'securite',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'crim_hom_vrel',
|
||||||
|
filters: { unit: 'RT' },
|
||||||
|
sens_positif: 'baisse',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Europe ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
{
|
||||||
|
id: 'soutien_ue',
|
||||||
|
label: "Soutien à l'appartenance à l'UE",
|
||||||
|
description: "Part estimée favorable à l'appartenance à l'UE (Eurobaromètre)",
|
||||||
|
unite: '%',
|
||||||
|
theme: 'europe',
|
||||||
|
source: 'eurostat',
|
||||||
|
dataset: 'eb_trust_inst_eu_ec_ep_2',
|
||||||
|
filters: { unit: 'PC', obs_status: 'T' },
|
||||||
|
sens_positif: 'hausse',
|
||||||
|
},
|
||||||
|
|
||||||
|
];
|
||||||
|
|
||||||
|
// Index par ID pour accès rapide
|
||||||
|
export const INDICATEURS_BY_ID = Object.fromEntries(
|
||||||
|
INDICATEURS.map(i => [i.id, i])
|
||||||
|
);
|
||||||
|
|
||||||
|
// Index par thème
|
||||||
|
export const INDICATEURS_BY_THEME: Record<string, IndicateurConfig[]> = {};
|
||||||
|
for (const ind of INDICATEURS) {
|
||||||
|
const themes = Array.isArray(ind.theme) ? ind.theme : [ind.theme];
|
||||||
|
for (const t of themes) {
|
||||||
|
if (!INDICATEURS_BY_THEME[t]) INDICATEURS_BY_THEME[t] = [];
|
||||||
|
INDICATEURS_BY_THEME[t].push(ind);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/lib/insee.ts
Normal file
105
src/lib/insee.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
/**
|
||||||
|
* Client INSEE BDM (Banque de Données Macroéconomiques)
|
||||||
|
* API SDMX publique — pas de clé requise pour les séries publiques.
|
||||||
|
* Doc: https://api.insee.fr/catalogue/site/themes/wso2/subthemes/insee/pages/item-info.jag?name=BDM&version=V1&provider=INSEE
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BASE = 'https://api.insee.fr/series/BDM/V1';
|
||||||
|
|
||||||
|
export interface INSEEValue {
|
||||||
|
value: number | null;
|
||||||
|
period: string; // ex: "2024-Q3" ou "2024"
|
||||||
|
unit?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SDMXObs {
|
||||||
|
[period: string]: [number | null, ...unknown[]];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SDMXSeries {
|
||||||
|
observations: SDMXObs;
|
||||||
|
attributes?: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SDMXDataset {
|
||||||
|
series?: Record<string, SDMXSeries>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch une série INSEE BDM par son identifiant.
|
||||||
|
* Retourne la dernière valeur disponible.
|
||||||
|
*/
|
||||||
|
export async function fetchSerie(serieId: string): Promise<INSEEValue> {
|
||||||
|
try {
|
||||||
|
const url = `${BASE}/data/SERIES_BDM/${serieId}?lastNObservations=4&format=sdmx-json`;
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json',
|
||||||
|
// Pas besoin de token pour les séries publiques
|
||||||
|
},
|
||||||
|
signal: AbortSignal.timeout(12_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
console.warn(`[insee] ${serieId} → HTTP ${res.status}`);
|
||||||
|
return { value: null, period: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
// Parcourir la structure SDMX-JSON
|
||||||
|
const datasets: SDMXDataset[] = data?.dataSets || [];
|
||||||
|
const firstDataset = datasets[0];
|
||||||
|
if (!firstDataset?.series) return { value: null, period: '' };
|
||||||
|
|
||||||
|
const firstSeries = Object.values(firstDataset.series)[0] as SDMXSeries;
|
||||||
|
if (!firstSeries?.observations) return { value: null, period: '' };
|
||||||
|
|
||||||
|
// Trouver la période la plus récente
|
||||||
|
const structure = data?.structure;
|
||||||
|
const timeDimension = structure?.dimensions?.observation?.find(
|
||||||
|
(d: { id: string }) => d.id === 'TIME_PERIOD'
|
||||||
|
);
|
||||||
|
|
||||||
|
const obsEntries = Object.entries(firstSeries.observations);
|
||||||
|
if (obsEntries.length === 0) return { value: null, period: '' };
|
||||||
|
|
||||||
|
// L'index SDMX-JSON → période via timeDimension.values
|
||||||
|
const lastEntry = obsEntries[obsEntries.length - 1];
|
||||||
|
const obsIdx = parseInt(lastEntry[0]);
|
||||||
|
const period = timeDimension?.values?.[obsIdx]?.id || lastEntry[0];
|
||||||
|
const value = (lastEntry[1] as unknown[])[0] as number | null;
|
||||||
|
|
||||||
|
return { value, period };
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`[insee] ${serieId} → ${(e as Error).message}`);
|
||||||
|
return { value: null, period: '' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Séries INSEE courantes (IDs BDM)
|
||||||
|
* Source: https://www.bdm.insee.fr/bdm2/choixCriteres?codeGroupe=CONSO_MENAGES
|
||||||
|
*/
|
||||||
|
export const SERIES_INSEE = {
|
||||||
|
// PIB
|
||||||
|
PIB_VOLUME_T: '010565692', // PIB volume, variation trimestrielle
|
||||||
|
PIB_VOLUME_A: '010565695', // PIB volume, variation annuelle
|
||||||
|
|
||||||
|
// Inflation
|
||||||
|
IPC_ENSEMBLE: '001759970', // IPC ensemble, variation annuelle
|
||||||
|
IPC_ALIMENTAIRE: '001759971', // IPC alimentation
|
||||||
|
|
||||||
|
// Emploi
|
||||||
|
TAUX_CHOMAGE: '001688526', // Taux de chômage BIT France métro
|
||||||
|
TAUX_EMPLOI: '001688527', // Taux d'emploi 15-64 ans
|
||||||
|
EMPLOI_TOTAL: '001688535', // Emploi total (milliers)
|
||||||
|
|
||||||
|
// Finances publiques
|
||||||
|
DEFICIT_PUBLIC: '001639745', // Déficit public % PIB
|
||||||
|
DETTE_PUBLIQUE: '001639746', // Dette publique % PIB (Maastricht)
|
||||||
|
|
||||||
|
// Ménages
|
||||||
|
POUVOIR_ACHAT: '010565799', // Pouvoir d'achat des ménages
|
||||||
|
TAUX_EPARGNE: '001637643', // Taux d'épargne des ménages
|
||||||
|
} as const;
|
||||||
222
src/pages/donnees/index.astro
Normal file
222
src/pages/donnees/index.astro
Normal file
|
|
@ -0,0 +1,222 @@
|
||||||
|
---
|
||||||
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
import KpiLive from '../../components/KpiLive.astro';
|
||||||
|
import { INDICATEURS_BY_THEME, PAYS_LABEL } from '../../lib/indicators-config.ts';
|
||||||
|
|
||||||
|
// Charger le JSON des indicateurs (généré en pré-build)
|
||||||
|
let indicateursData: any = { indicators: [], insee: {}, generated_at: null };
|
||||||
|
try {
|
||||||
|
const raw = await import('../../data/live/indicators.json');
|
||||||
|
indicateursData = raw.default || raw;
|
||||||
|
} catch {
|
||||||
|
// Données non disponibles (premier build sans fetch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Indexer par ID pour accès rapide
|
||||||
|
const byId = Object.fromEntries(
|
||||||
|
(indicateursData.indicators || []).map((i: any) => [i.id, i])
|
||||||
|
);
|
||||||
|
|
||||||
|
const generatedAt = indicateursData.generated_at
|
||||||
|
? new Date(indicateursData.generated_at).toLocaleDateString('fr-FR', { day: 'numeric', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' })
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Groupes d'indicateurs pour le dashboard
|
||||||
|
const GROUPES = [
|
||||||
|
{
|
||||||
|
titre: 'Finances publiques',
|
||||||
|
couleur: '#1e3a6e',
|
||||||
|
bg: '#eff6ff',
|
||||||
|
ids: ['deficit_pib', 'dette_pib', 'depenses_publiques_pib', 'prelevements_obligatoires', 'croissance_pib', 'depenses_sociales_pib'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Emploi & Marché du travail',
|
||||||
|
couleur: '#7c3aed',
|
||||||
|
bg: '#f5f3ff',
|
||||||
|
ids: ['taux_emploi', 'taux_chomage', 'taux_chomage_jeunes', 'taux_emploi_seniors'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Santé',
|
||||||
|
couleur: '#dc2626',
|
||||||
|
bg: '#fef2f2',
|
||||||
|
ids: ['esperance_vie', 'depenses_sante_pib'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Éducation',
|
||||||
|
couleur: '#0891b2',
|
||||||
|
bg: '#ecfeff',
|
||||||
|
ids: ['decrochage_scolaire', 'diplomes_superieurs', 'depenses_education_pib'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Cohésion sociale',
|
||||||
|
couleur: '#d97706',
|
||||||
|
bg: '#fffbeb',
|
||||||
|
ids: ['gini', 'taux_pauvrete', 'taux_pauvrete_enfants'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Logement',
|
||||||
|
couleur: '#059669',
|
||||||
|
bg: '#f0fdf4',
|
||||||
|
ids: ['indice_prix_immobilier', 'surcharge_logement', 'taux_proprietaires'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
titre: 'Environnement & Énergie',
|
||||||
|
couleur: '#16a34a',
|
||||||
|
bg: '#f0fdf4',
|
||||||
|
ids: ['emissions_ghg_per_capita', 'part_energies_renouvelables', 'intensite_energetique'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const PAYS_ORDRE = ['FR', 'DE', 'ES', 'IT', 'SE', 'NL', 'EU27_2020'];
|
||||||
|
const INDICATEURS_TABLE = ['deficit_pib', 'dette_pib', 'depenses_publiques_pib', 'taux_chomage', 'taux_emploi', 'esperance_vie', 'gini', 'emissions_ghg_per_capita'];
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Tableau de bord France — IRPF"
|
||||||
|
description="Indicateurs clés de la France comparés à ses voisins européens. Données Eurostat en temps réel."
|
||||||
|
>
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<section style="background: linear-gradient(135deg, #0f172a 0%, #1e3a6e 100%); color: white; padding: 3rem 1.5rem 2rem;">
|
||||||
|
<div style="max-width: 1200px; margin: 0 auto;">
|
||||||
|
<div style="font-size: 0.75rem; letter-spacing: 0.1em; color: #93c5fd; margin-bottom: 0.75rem;">IRPF — DONNÉES OUVERTES</div>
|
||||||
|
<h1 style="font-family: 'Playfair Display', serif; font-size: 2.25rem; margin-bottom: 1rem;">Tableau de bord France</h1>
|
||||||
|
<p style="color: #cbd5e1; max-width: 650px; line-height: 1.7; margin-bottom: 1.5rem;">
|
||||||
|
Indicateurs clés de la France comparés à l'Allemagne, l'Espagne, l'Italie, la Suède, les Pays-Bas et la moyenne UE27.
|
||||||
|
Sources : Eurostat API (données ouvertes) + INSEE BDM.
|
||||||
|
</p>
|
||||||
|
{generatedAt && (
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem; font-size: 0.75rem; color: #94a3b8;">
|
||||||
|
<span style="width: 8px; height: 8px; background: #22c55e; border-radius: 50;"></span>
|
||||||
|
Mis à jour le {generatedAt} — {indicateursData.indicators?.length || 0} indicateurs collectés
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Navigation indicateurs -->
|
||||||
|
<div style="background: white; border-bottom: 1px solid #e5e7eb; position: sticky; top: 0; z-index: 10;">
|
||||||
|
<div style="max-width: 1200px; margin: 0 auto; padding: 0 1.5rem; overflow-x: auto;">
|
||||||
|
<nav style="display: flex; gap: 0; white-space: nowrap;">
|
||||||
|
{GROUPES.map(g => (
|
||||||
|
<a href={`#${g.titre.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`}
|
||||||
|
style="padding: 0.875rem 1rem; font-size: 0.8rem; font-weight: 500; color: #6b7280; text-decoration: none; border-bottom: 2px solid transparent; display: inline-block;">
|
||||||
|
{g.titre}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<a href="#comparaison" style="padding: 0.875rem 1rem; font-size: 0.8rem; font-weight: 500; color: #1e3a6e; text-decoration: none; border-bottom: 2px solid #1e3a6e;">
|
||||||
|
Comparatif EU →
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="max-width: 1200px; margin: 0 auto; padding: 3rem 1.5rem;">
|
||||||
|
|
||||||
|
<!-- Groupes d'indicateurs -->
|
||||||
|
{GROUPES.map(groupe => {
|
||||||
|
const inds = groupe.ids.map(id => byId[id]).filter(Boolean);
|
||||||
|
if (inds.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<section id={groupe.titre.toLowerCase().replace(/[^a-z0-9]+/g, '-')} style="margin-bottom: 3rem;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.25rem;">
|
||||||
|
<div style={`width: 3px; height: 1.5rem; background: ${groupe.couleur}; border-radius: 2px;`}></div>
|
||||||
|
<h2 style={`font-size: 1.1rem; font-weight: 700; color: ${groupe.couleur}; margin: 0;`}>{groupe.titre}</h2>
|
||||||
|
<div style="flex: 1; height: 1px; background: #e5e7eb;"></div>
|
||||||
|
</div>
|
||||||
|
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75rem;">
|
||||||
|
{inds.map((ind: any) => (
|
||||||
|
<KpiLive
|
||||||
|
label={ind.label}
|
||||||
|
valeur={ind.france?.value ?? null}
|
||||||
|
annee={ind.france?.year || ''}
|
||||||
|
unite={ind.unite}
|
||||||
|
sens_positif={ind.sens_positif}
|
||||||
|
pays={ind.pays}
|
||||||
|
note={ind.note}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
<!-- Tableau comparatif -->
|
||||||
|
<section id="comparaison" style="margin-top: 2rem;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1.5rem;">
|
||||||
|
<h2 style="font-family: 'Playfair Display', serif; font-size: 1.5rem; color: #0f172a; margin: 0;">Comparatif France — Europe</h2>
|
||||||
|
<div style="flex: 1; height: 1px; background: #e5e7eb;"></div>
|
||||||
|
<span style="font-size: 0.75rem; color: #9ca3af;">Source : Eurostat API</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="overflow-x: auto; border-radius: 0.5rem; border: 1px solid #e5e7eb;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.82rem;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #f8fafc; border-bottom: 2px solid #e5e7eb;">
|
||||||
|
<th style="text-align: left; padding: 0.75rem 1rem; font-weight: 600; color: #374151; min-width: 200px;">Indicateur</th>
|
||||||
|
{PAYS_ORDRE.map(code => (
|
||||||
|
<th style="text-align: right; padding: 0.75rem 0.75rem; font-weight: 600; color: code === 'FR' ? '#1e3a6e' : '#374151; min-width: 80px;">
|
||||||
|
{code === 'EU27_2020' ? 'UE27' : code}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{INDICATEURS_TABLE.map((id, idx) => {
|
||||||
|
const ind = byId[id];
|
||||||
|
if (!ind) return null;
|
||||||
|
return (
|
||||||
|
<tr style={`border-bottom: 1px solid #e5e7eb; background: ${idx % 2 === 0 ? 'white' : '#f8fafc'};`}>
|
||||||
|
<td style="padding: 0.625rem 1rem; font-weight: 500; color: #374151;">
|
||||||
|
{ind.label}
|
||||||
|
<div style="font-size: 0.7rem; color: #9ca3af; font-weight: 400;">{ind.unite}</div>
|
||||||
|
</td>
|
||||||
|
{PAYS_ORDRE.map(code => {
|
||||||
|
const pv = code === 'FR' ? ind.france : ind.pays?.[code];
|
||||||
|
const val = pv?.value;
|
||||||
|
const formatted = val !== null && val !== undefined
|
||||||
|
? (Math.abs(val) >= 100 ? Math.round(val).toLocaleString('fr-FR') : val.toFixed(1))
|
||||||
|
: '—';
|
||||||
|
const isFr = code === 'FR';
|
||||||
|
return (
|
||||||
|
<td style={`text-align: right; padding: 0.625rem 0.75rem; font-weight: ${isFr ? '700' : '400'}; color: ${isFr ? '#1e3a6e' : '#374151'};`}>
|
||||||
|
{formatted}
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p style="font-size: 0.72rem; color: #9ca3af; margin-top: 0.75rem;">
|
||||||
|
Dernière mise à jour : {generatedAt || 'N/A'} — Eurostat API JSON-stat 2.0
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Note méthodologique -->
|
||||||
|
<section style="background: #f8fafc; border: 1px solid #e5e7eb; border-radius: 0.5rem; padding: 1.5rem; margin-top: 2rem;">
|
||||||
|
<h3 style="font-size: 0.9rem; font-weight: 700; color: #374151; margin-bottom: 0.75rem;">Sources et méthodologie</h3>
|
||||||
|
<div style="font-size: 0.8rem; color: #6b7280; line-height: 1.7; display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
||||||
|
<div>
|
||||||
|
<strong>Eurostat</strong> — API JSON-stat 2.0, données officielles de l'Office statistique de l'Union européenne.
|
||||||
|
Mise à jour automatique à chaque rebuild du site (max 6h de délai).
|
||||||
|
CORS ouvert, pas d'authentification requise.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Comparaison</strong> — France vs Allemagne, Espagne, Italie, Suède, Pays-Bas et moyenne UE27.
|
||||||
|
Les valeurs <span style="color: #dc2626;">en rouge</span> indiquent un retard par rapport à la moyenne UE ;
|
||||||
|
les valeurs <span style="color: #16a34a;">en vert</span> indiquent une avance.
|
||||||
|
Données non disponibles (N/D) = non publiées par Eurostat pour cette période.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
th { white-space: nowrap; }
|
||||||
|
</style>
|
||||||
134
src/scripts/fetch-indicators.ts
Normal file
134
src/scripts/fetch-indicators.ts
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
/**
|
||||||
|
* Script de collecte des indicateurs open data
|
||||||
|
* Exécuté en pre-build : génère src/data/live/indicators.json
|
||||||
|
*
|
||||||
|
* Sources :
|
||||||
|
* - Eurostat API (CORS ouvert, JSON-stat 2.0)
|
||||||
|
* - INSEE BDM API (SDMX-JSON, public)
|
||||||
|
*
|
||||||
|
* Usage :
|
||||||
|
* npx tsx src/scripts/fetch-indicators.ts
|
||||||
|
* npm run fetch-data
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { writeFileSync, mkdirSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
import { fetchIndicateur } from '../lib/eurostat.js';
|
||||||
|
import { fetchSerie, SERIES_INSEE } from '../lib/insee.js';
|
||||||
|
import { INDICATEURS, PAYS_COMPARES, PAYS_LABEL } from '../lib/indicators-config.js';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const OUT_DIR = join(__dirname, '../../src/data/live');
|
||||||
|
const OUT_FILE = join(OUT_DIR, 'indicators.json');
|
||||||
|
|
||||||
|
// ─── TYPES SORTIE ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
interface IndicateurResult {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
unite: string;
|
||||||
|
theme: string | string[];
|
||||||
|
derniere_maj: string;
|
||||||
|
france: { value: number | null; year: string };
|
||||||
|
pays: Record<string, { value: number | null; year: string; label: string }>;
|
||||||
|
sens_positif?: 'hausse' | 'baisse';
|
||||||
|
note?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndicateursJSON {
|
||||||
|
generated_at: string;
|
||||||
|
source: string;
|
||||||
|
indicators: IndicateurResult[];
|
||||||
|
insee: Record<string, { value: number | null; period: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── FETCH ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
console.log('[fetch-indicators] Démarrage...');
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const indicators: IndicateurResult[] = [];
|
||||||
|
const inseeData: Record<string, { value: number | null; period: string }> = {};
|
||||||
|
|
||||||
|
// 1. Eurostat — fetch en parallèle par batch de 5
|
||||||
|
console.log(`[fetch-indicators] Fetch ${INDICATEURS.filter(i => i.source === 'eurostat').length} indicateurs Eurostat...`);
|
||||||
|
|
||||||
|
const eurostatInds = INDICATEURS.filter(i => i.source === 'eurostat' && i.dataset);
|
||||||
|
const BATCH = 5;
|
||||||
|
|
||||||
|
for (let i = 0; i < eurostatInds.length; i += BATCH) {
|
||||||
|
const batch = eurostatInds.slice(i, i + BATCH);
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
batch.map(ind =>
|
||||||
|
fetchIndicateur(
|
||||||
|
ind.dataset!,
|
||||||
|
ind.filters || {},
|
||||||
|
[...PAYS_COMPARES],
|
||||||
|
).then(geoValues => ({ ind, geoValues }))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const res of results) {
|
||||||
|
if (res.status === 'rejected') continue;
|
||||||
|
const { ind, geoValues } = res.value;
|
||||||
|
|
||||||
|
const fr = geoValues['FR'] || { value: null, year: '' };
|
||||||
|
const pays: IndicateurResult['pays'] = {};
|
||||||
|
for (const [code, val] of Object.entries(geoValues)) {
|
||||||
|
pays[code] = { ...val, label: PAYS_LABEL[code] || code };
|
||||||
|
}
|
||||||
|
|
||||||
|
indicators.push({
|
||||||
|
id: ind.id,
|
||||||
|
label: ind.label,
|
||||||
|
unite: ind.unite,
|
||||||
|
theme: ind.theme,
|
||||||
|
derniere_maj: new Date().toISOString().slice(0, 10),
|
||||||
|
france: fr,
|
||||||
|
pays,
|
||||||
|
sens_positif: ind.sens_positif,
|
||||||
|
note: ind.note,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` ✓ ${ind.id}: FR=${fr.value !== null ? fr.value : 'N/D'} (${fr.year})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. INSEE — fetch séries nationales
|
||||||
|
console.log(`[fetch-indicators] Fetch ${Object.keys(SERIES_INSEE).length} séries INSEE...`);
|
||||||
|
const inseeResults = await Promise.allSettled(
|
||||||
|
Object.entries(SERIES_INSEE).map(async ([key, id]) => {
|
||||||
|
const val = await fetchSerie(id);
|
||||||
|
return { key, val };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const res of inseeResults) {
|
||||||
|
if (res.status === 'rejected') continue;
|
||||||
|
inseeData[res.value.key] = res.value.val;
|
||||||
|
console.log(` ✓ INSEE.${res.value.key}: ${res.value.val.value} (${res.value.val.period})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Écriture du fichier JSON
|
||||||
|
mkdirSync(OUT_DIR, { recursive: true });
|
||||||
|
|
||||||
|
const output: IndicateursJSON = {
|
||||||
|
generated_at: new Date().toISOString(),
|
||||||
|
source: 'Eurostat API + INSEE BDM API',
|
||||||
|
indicators,
|
||||||
|
insee: inseeData,
|
||||||
|
};
|
||||||
|
|
||||||
|
writeFileSync(OUT_FILE, JSON.stringify(output, null, 2), 'utf-8');
|
||||||
|
|
||||||
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||||
|
console.log(`[fetch-indicators] ✅ ${indicators.length} indicateurs Eurostat + ${Object.keys(inseeData).length} séries INSEE → ${OUT_FILE} (${elapsed}s)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(err => {
|
||||||
|
console.error('[fetch-indicators] ERREUR:', err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue