1

Page de statistiques

This commit is contained in:
2025-11-28 01:47:10 +01:00
parent 38926267a3
commit fd27dc7fb6
47 changed files with 3278 additions and 86 deletions

357
tools/generate_stats.js Normal file
View File

@@ -0,0 +1,357 @@
#!/usr/bin/env node
const fs = require("fs/promises");
const path = require("path");
const DEFAULT_CONFIG_PATH = "tools/stats.json";
const DEFAULT_DATA_OUTPUT = "content/stats/data/stats.json";
const DEFAULT_IMAGE_DIR = "content/stats/images";
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 1) {
const current = argv[index];
const next = argv[index + 1];
switch (current) {
case "--config":
case "-c":
args.config = next;
index += 1;
break;
case "--data":
case "-d":
args.data = next;
index += 1;
break;
case "--only":
case "-o":
args.only = next;
index += 1;
break;
default:
break;
}
}
return args;
}
async function loadDefinition(configPath) {
const raw = await fs.readFile(configPath, "utf8");
try {
return JSON.parse(raw);
} catch (error) {
throw new Error(`Impossible de parser ${configPath}: ${error.message}`);
}
}
async function loadModule(scriptPath) {
const resolved = path.resolve(scriptPath);
await fs.access(resolved);
// allow re-run without cache
delete require.cache[resolved];
const mod = require(resolved);
if (!mod || typeof mod.run !== "function") {
throw new Error(`Le script ${scriptPath} doit exporter une fonction run(context)`);
}
return mod;
}
function resolvePythonInterpreter() {
const envPython = process.env.VIRTUAL_ENV
? path.join(process.env.VIRTUAL_ENV, "bin", "python")
: null;
const candidates = [
envPython,
path.join(process.cwd(), ".venv", "bin", "python"),
path.join(process.cwd(), ".venv", "bin", "python3"),
"python3",
].filter(Boolean);
return candidates.find((candidate) => {
try {
const stat = require("fs").statSync(candidate);
return stat.isFile() || stat.isSymbolicLink();
} catch (_error) {
return false;
}
}) || "python3";
}
async function runPython(scriptPath, payload) {
const resolved = path.resolve(scriptPath);
await fs.access(resolved);
const interpreter = resolvePythonInterpreter();
return new Promise((resolve, reject) => {
const child = require("child_process").spawn(interpreter, [resolved], {
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (data) => {
stdout += data.toString();
});
child.stderr.on("data", (data) => {
stderr += data.toString();
});
child.on("error", (error) => {
reject(error);
});
child.on("close", (code) => {
if (code !== 0) {
const err = new Error(stderr || `Python exited with code ${code}`);
err.code = code;
return reject(err);
}
const trimmed = stdout.trim();
if (!trimmed) return resolve({});
try {
resolve(JSON.parse(trimmed));
} catch (error) {
reject(new Error(`Invalid JSON from ${scriptPath}: ${error.message}`));
}
});
child.stdin.write(JSON.stringify(payload));
child.stdin.end();
});
}
function toPublicPath(target, { rootDir = process.cwd(), staticDir = path.resolve("static"), absOutput } = {}) {
if (!target) return "";
const normalized = target.replace(/\\/g, "/");
if (/^https?:\/\//i.test(normalized)) return normalized;
if (normalized.startsWith("/")) {
return normalized.replace(/\/{2,}/g, "/");
}
const absolute = absOutput || path.resolve(rootDir, target);
if (absolute.startsWith(staticDir)) {
const rel = path.relative(staticDir, absolute).replace(/\\/g, "/");
return `/${rel}`;
}
if (!path.isAbsolute(target) && !target.startsWith("/")) {
return normalized;
}
const relRoot = path.relative(rootDir, absolute).replace(/\\/g, "/");
return `/${relRoot}`;
}
function resolveGraphicPaths(stat, defaultImageDir, { rootDir, staticDir }) {
const target = stat.image || (defaultImageDir ? path.join(defaultImageDir, `${stat.key}.png`) : null);
if (!target) {
throw new Error("Chemin d'image manquant (image ou defaultImageDir)");
}
const absOutput = path.isAbsolute(target) ? target : path.resolve(rootDir, target);
const publicPath = toPublicPath(target, { rootDir, staticDir, absOutput });
return { publicPath, outputPath: absOutput };
}
function mergeResult(base, result, { publicPath } = {}) {
const entry = { ...base };
if (publicPath && base.type === "graphic") {
entry.image = publicPath;
}
if (result === undefined || result === null) {
return entry;
}
if (typeof result === "object" && !Array.isArray(result)) {
if (Object.prototype.hasOwnProperty.call(result, "value")) {
entry.value = result.value;
}
if (result.image) {
entry.image = toPublicPath(result.image);
}
if (result.meta) {
entry.meta = result.meta;
}
if (result.data) {
entry.data = result.data;
}
return entry;
}
entry.value = result;
return entry;
}
async function runStat(stat, context) {
const base = {
key: stat.key,
title: stat.title,
type: stat.type,
};
if (!stat.key) {
throw new Error("Cle manquante pour cette statistique");
}
if (!stat.script) {
throw new Error(`Script manquant pour ${stat.key}`);
}
if (!stat.type) {
throw new Error(`Type manquant pour ${stat.key}`);
}
const isPython = stat.script.endsWith(".py");
if (isPython) {
if (stat.type === "graphic") {
const { publicPath, outputPath } = resolveGraphicPaths(stat, context.defaultImageDir, context);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
const result = await runPython(stat.script, {
...context,
stat,
outputPath,
publicPath,
});
return mergeResult(base, result, { publicPath });
}
if (stat.type === "variable") {
const result = await runPython(stat.script, {
...context,
stat,
});
return mergeResult(base, result);
}
throw new Error(`Type inconnu pour ${stat.key}: ${stat.type}`);
} else {
const mod = await loadModule(stat.script);
if (stat.type === "graphic") {
const { publicPath, outputPath } = resolveGraphicPaths(stat, context.defaultImageDir, context);
await fs.mkdir(path.dirname(outputPath), { recursive: true });
const result = await mod.run({
...context,
stat,
outputPath,
publicPath,
});
return mergeResult(base, result, { publicPath });
}
if (stat.type === "variable") {
const result = await mod.run({
...context,
stat,
});
return mergeResult(base, result);
}
throw new Error(`Type inconnu pour ${stat.key}: ${stat.type}`);
}
}
function buildOnlyFilter(onlyRaw) {
if (!onlyRaw) return null;
const parts = onlyRaw
.split(",")
.map((part) => part.trim())
.filter(Boolean);
return new Set(parts);
}
async function main() {
const cliArgs = parseArgs(process.argv.slice(2));
const definitionPath = path.resolve(cliArgs.config || DEFAULT_CONFIG_PATH);
const definition = await loadDefinition(definitionPath);
const statsConfig = definition.config || {};
const dataOutput = path.resolve(cliArgs.data || statsConfig.dataOutput || DEFAULT_DATA_OUTPUT);
const defaultImageDir = statsConfig.defaultImageDir || DEFAULT_IMAGE_DIR;
const onlyFilter = buildOnlyFilter(cliArgs.only);
const context = {
rootDir: process.cwd(),
contentDir: path.resolve("content"),
staticDir: path.resolve("static"),
definitionPath,
defaultImageDir,
config: statsConfig,
};
const output = {
generated_at: new Date().toISOString(),
sections: [],
};
const errors = [];
for (const section of definition.sections || []) {
const results = [];
for (const stat of section.statistics || []) {
if (onlyFilter && !onlyFilter.has(stat.key)) {
continue;
}
try {
const entry = await runStat(stat, context);
results.push(entry);
console.log(`[ok] ${stat.key}`);
} catch (error) {
errors.push({ key: stat.key, message: error.message });
console.error(`[err] ${stat.key}: ${error.message}`);
results.push({
key: stat.key,
title: stat.title,
type: stat.type,
error: error.message,
image: stat.image ? toPublicPath(stat.image) : undefined,
});
}
}
if (results.length > 0) {
output.sections.push({
title: section.title,
statistics: results,
});
}
}
if (errors.length > 0) {
output.errors = errors;
}
await fs.mkdir(path.dirname(dataOutput), { recursive: true });
await fs.writeFile(dataOutput, `${JSON.stringify(output, null, 2)}\n`, "utf8");
const relativeOutput = path.relative(process.cwd(), dataOutput);
console.log(`\nFichier de donnees genere: ${relativeOutput}`);
if (errors.length > 0) {
console.log(`Statistiques en erreur: ${errors.length}. Les entrees concernees contiennent le message d'erreur.`);
}
}
main().catch((error) => {
console.error(error.message);
process.exit(1);
});