Dé-Replit-isation complète du projet
Supprimés : - replit.md — doc Replit obsolète - docs/GITEA_TUTO.md — tuto push Replit → Gitea (obsolète) - artifacts/api-server/ — serveur TypeScript mort, remplacé par Flask - artifacts/voix-du-peuple/vite.config.selfhost.ts — fusionné dans vite.config.ts Nettoyés : - ai_agent.py — fallback Replit AI supprimé (Mistral + OpenAI-compatible suffisent) - vite.config.ts — plugins @replit/* retirés, PORT optionnel (défaut 5173) - package.json + pnpm-workspace.yaml — @replit/* retirés du catalog et des deps - badge.tsx + button.tsx — commentaires // @replit supprimés - README.md, DEPLOIEMENT.md, DAT.md, DEX.md, WIKI.md — références Replit remplacées Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+6
-9
@@ -18,7 +18,7 @@ Internet
|
||||
- **Frontend** : React + Vite, servi comme fichiers statiques par Nginx
|
||||
- **Backend** : Flask + Gunicorn (4 workers), accessible uniquement via Nginx
|
||||
- **Base de données** : PostgreSQL 15+
|
||||
- **IA** : API OpenAI (clé standard, pas de proxy Replit)
|
||||
- **IA** : Mistral AI (MISTRAL_API_KEY requis)
|
||||
|
||||
---
|
||||
|
||||
@@ -295,8 +295,7 @@ voix-du-peuple/
|
||||
│ │ ├── pages/ # home.tsx, about.tsx
|
||||
│ │ ├── components/ # Composants UI (shadcn/ui + radix)
|
||||
│ │ └── App.tsx # Routing principal
|
||||
│ ├── vite.config.ts # Config Replit (développement)
|
||||
│ └── vite.config.selfhost.ts # Config auto-hébergement (production)
|
||||
│ └── vite.config.ts # Config Vite (développement + production)
|
||||
├── lib/
|
||||
│ ├── api-spec/ # Spécification OpenAPI
|
||||
│ ├── api-client-react/ # Hooks React Query générés
|
||||
@@ -327,12 +326,10 @@ voix-du-peuple/
|
||||
|
||||
## Modèles IA utilisés
|
||||
|
||||
| Fonction | Modèle par défaut | Configurable dans |
|
||||
|----------|-------------------|-------------------|
|
||||
| Filtrage des idées | `gpt-4o-mini` | `ai_agent.py` ligne 56 |
|
||||
| Synthèse collective | `gpt-4o` | `ai_agent.py` ligne 104 |
|
||||
|
||||
> **Note :** Les modèles `gpt-5-mini` et `gpt-5.2` sont les noms utilisés sur la plateforme Replit. En auto-hébergement avec l'API OpenAI standard, utilisez `gpt-4o-mini` et `gpt-4o`.
|
||||
| Fonction | Modèle par défaut | Variable d'environnement |
|
||||
|----------|-------------------|--------------------------|
|
||||
| Filtrage des idées | `mistral-small-latest` | `FILTER_MODEL` |
|
||||
| Synthèse collective | `mistral-large-latest` | `SYNTHESIS_MODEL` |
|
||||
|
||||
Pour changer les modèles, éditez `artifacts/flask-api/ai_agent.py` :
|
||||
```python
|
||||
|
||||
@@ -129,16 +129,6 @@ systemctl reload nginx
|
||||
|
||||
---
|
||||
|
||||
## Synchroniser avec ce dépôt (depuis Replit)
|
||||
|
||||
```bash
|
||||
bash scripts/push-gitea.sh
|
||||
```
|
||||
|
||||
Prérequis : secret `GITEA_TOKEN` configuré dans Replit → Secrets.
|
||||
|
||||
---
|
||||
|
||||
## Documentation
|
||||
|
||||
| Document | Contenu |
|
||||
@@ -147,8 +137,9 @@ Prérequis : secret `GITEA_TOKEN` configuré dans Replit → Secrets.
|
||||
| [`docs/DEX.md`](docs/DEX.md) | Guide d'exploitation et maintenance |
|
||||
| [`docs/WIKI.md`](docs/WIKI.md) | Page wiki — présentation générale |
|
||||
| [`docs/INSTALL_ROCKY.md`](docs/INSTALL_ROCKY.md) | Installation sur RockyLinux 9 |
|
||||
| [`docs/GITEA_TUTO.md`](docs/GITEA_TUTO.md) | Synchronisation Replit → Gitea |
|
||||
| [`docs/SECURITE_ANTI_ABUS.md`](docs/SECURITE_ANTI_ABUS.md) | Protections anti-bot, flood, rate limiting, hCaptcha |
|
||||
| [`docs/RGPD.md`](docs/RGPD.md) | Conformité RGPD — registre des traitements |
|
||||
| [`docs/PROMPTS_IA.md`](docs/PROMPTS_IA.md) | Prompts IA intégraux (transparence algorithmique) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
kind = "api"
|
||||
previewPath = "/api"
|
||||
title = "API Server (Flask)"
|
||||
version = "1.0.0"
|
||||
id = "3B4_FFSkEVBkAeYMFRJ2e"
|
||||
|
||||
[[services]]
|
||||
localPort = 8080
|
||||
name = "API Server"
|
||||
paths = ["/api"]
|
||||
|
||||
[services.development]
|
||||
run = "PORT=8080 sh /home/runner/workspace/artifacts/flask-api/start.sh"
|
||||
|
||||
[services.production]
|
||||
|
||||
[services.production.build]
|
||||
args = ["echo", "no build step for Flask"]
|
||||
|
||||
[services.production.run]
|
||||
args = ["sh", "/home/runner/workspace/artifacts/flask-api/start.sh"]
|
||||
|
||||
[services.production.run.env]
|
||||
PORT = "8080"
|
||||
|
||||
[services.production.health.startup]
|
||||
path = "/api/healthz"
|
||||
@@ -1,126 +0,0 @@
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { build as esbuild } from "esbuild";
|
||||
import esbuildPluginPino from "esbuild-plugin-pino";
|
||||
import { rm } from "node:fs/promises";
|
||||
|
||||
// Plugins (e.g. 'esbuild-plugin-pino') may use `require` to resolve dependencies
|
||||
globalThis.require = createRequire(import.meta.url);
|
||||
|
||||
const artifactDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function buildAll() {
|
||||
const distDir = path.resolve(artifactDir, "dist");
|
||||
await rm(distDir, { recursive: true, force: true });
|
||||
|
||||
await esbuild({
|
||||
entryPoints: [path.resolve(artifactDir, "src/index.ts")],
|
||||
platform: "node",
|
||||
bundle: true,
|
||||
format: "esm",
|
||||
outdir: distDir,
|
||||
outExtension: { ".js": ".mjs" },
|
||||
logLevel: "info",
|
||||
// Some packages may not be bundleable, so we externalize them, we can add more here as needed.
|
||||
// Some of the packages below may not be imported or installed, but we're adding them in case they are in the future.
|
||||
// Examples of unbundleable packages:
|
||||
// - uses native modules and loads them dynamically (e.g. sharp)
|
||||
// - use path traversal to read files (e.g. @google-cloud/secret-manager loads sibling .proto files)
|
||||
external: [
|
||||
"*.node",
|
||||
"sharp",
|
||||
"better-sqlite3",
|
||||
"sqlite3",
|
||||
"canvas",
|
||||
"bcrypt",
|
||||
"argon2",
|
||||
"fsevents",
|
||||
"re2",
|
||||
"farmhash",
|
||||
"xxhash-addon",
|
||||
"bufferutil",
|
||||
"utf-8-validate",
|
||||
"ssh2",
|
||||
"cpu-features",
|
||||
"dtrace-provider",
|
||||
"isolated-vm",
|
||||
"lightningcss",
|
||||
"pg-native",
|
||||
"oracledb",
|
||||
"mongodb-client-encryption",
|
||||
"nodemailer",
|
||||
"handlebars",
|
||||
"knex",
|
||||
"typeorm",
|
||||
"protobufjs",
|
||||
"onnxruntime-node",
|
||||
"@tensorflow/*",
|
||||
"@prisma/client",
|
||||
"@mikro-orm/*",
|
||||
"@grpc/*",
|
||||
"@swc/*",
|
||||
"@aws-sdk/*",
|
||||
"@azure/*",
|
||||
"@opentelemetry/*",
|
||||
"@google-cloud/*",
|
||||
"@google/*",
|
||||
"googleapis",
|
||||
"firebase-admin",
|
||||
"@parcel/watcher",
|
||||
"@sentry/profiling-node",
|
||||
"@tree-sitter/*",
|
||||
"aws-sdk",
|
||||
"classic-level",
|
||||
"dd-trace",
|
||||
"ffi-napi",
|
||||
"grpc",
|
||||
"hiredis",
|
||||
"kerberos",
|
||||
"leveldown",
|
||||
"miniflare",
|
||||
"mysql2",
|
||||
"newrelic",
|
||||
"odbc",
|
||||
"piscina",
|
||||
"realm",
|
||||
"ref-napi",
|
||||
"rocksdb",
|
||||
"sass-embedded",
|
||||
"sequelize",
|
||||
"serialport",
|
||||
"snappy",
|
||||
"tinypool",
|
||||
"usb",
|
||||
"workerd",
|
||||
"wrangler",
|
||||
"zeromq",
|
||||
"zeromq-prebuilt",
|
||||
"playwright",
|
||||
"puppeteer",
|
||||
"puppeteer-core",
|
||||
"electron",
|
||||
],
|
||||
sourcemap: "linked",
|
||||
plugins: [
|
||||
// pino relies on workers to handle logging, instead of externalizing it we use a plugin to handle it
|
||||
esbuildPluginPino({ transports: ["pino-pretty"] })
|
||||
],
|
||||
// Make sure packages that are cjs only (e.g. express) but are bundled continue to work in our esm output file
|
||||
banner: {
|
||||
js: `import { createRequire as __bannerCrReq } from 'node:module';
|
||||
import __bannerPath from 'node:path';
|
||||
import __bannerUrl from 'node:url';
|
||||
|
||||
globalThis.require = __bannerCrReq(import.meta.url);
|
||||
globalThis.__filename = __bannerUrl.fileURLToPath(import.meta.url);
|
||||
globalThis.__dirname = __bannerPath.dirname(globalThis.__filename);
|
||||
`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
buildAll().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"name": "@workspace/api-server",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "export NODE_ENV=development && pnpm run build && pnpm run start",
|
||||
"build": "node ./build.mjs",
|
||||
"start": "node --enable-source-maps ./dist/index.mjs",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@workspace/api-zod": "workspace:*",
|
||||
"@workspace/db": "workspace:*",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2",
|
||||
"drizzle-orm": "catalog:",
|
||||
"express": "^5",
|
||||
"openai": "^6.33.0",
|
||||
"pino": "^9",
|
||||
"pino-http": "^10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "^1.4.10",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "catalog:",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild-plugin-pino": "^2.3.3",
|
||||
"pino-pretty": "^13",
|
||||
"thread-stream": "3.1.0"
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import express, { type Express } from "express";
|
||||
import cors from "cors";
|
||||
import pinoHttp from "pino-http";
|
||||
import router from "./routes";
|
||||
import { logger } from "./lib/logger";
|
||||
|
||||
const app: Express = express();
|
||||
|
||||
app.use(
|
||||
pinoHttp({
|
||||
logger,
|
||||
serializers: {
|
||||
req(req) {
|
||||
return {
|
||||
id: req.id,
|
||||
method: req.method,
|
||||
url: req.url?.split("?")[0],
|
||||
};
|
||||
},
|
||||
res(res) {
|
||||
return {
|
||||
statusCode: res.statusCode,
|
||||
};
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
app.use("/api", router);
|
||||
|
||||
export default app;
|
||||
@@ -1,25 +0,0 @@
|
||||
import app from "./app";
|
||||
import { logger } from "./lib/logger";
|
||||
|
||||
const rawPort = process.env["PORT"];
|
||||
|
||||
if (!rawPort) {
|
||||
throw new Error(
|
||||
"PORT environment variable is required but was not provided.",
|
||||
);
|
||||
}
|
||||
|
||||
const port = Number(rawPort);
|
||||
|
||||
if (Number.isNaN(port) || port <= 0) {
|
||||
throw new Error(`Invalid PORT value: "${rawPort}"`);
|
||||
}
|
||||
|
||||
app.listen(port, (err) => {
|
||||
if (err) {
|
||||
logger.error({ err }, "Error listening on port");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
logger.info({ port }, "Server listening");
|
||||
});
|
||||
@@ -1,93 +0,0 @@
|
||||
import OpenAI from "openai";
|
||||
import { logger } from "./logger";
|
||||
|
||||
const openai = new OpenAI({
|
||||
baseURL: process.env.AI_INTEGRATIONS_OPENAI_BASE_URL,
|
||||
apiKey: process.env.AI_INTEGRATIONS_OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
export interface FilterResult {
|
||||
accepted: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export async function filterIdea(content: string): Promise<FilterResult> {
|
||||
const systemPrompt = `Tu es un agent de filtrage éthique pour une plateforme démocratique citoyenne.
|
||||
Ta mission : analyser les idées politiques soumises et décider si elles respectent les valeurs démocratiques.
|
||||
|
||||
Critères d'ACCEPTATION :
|
||||
- L'idée promeut les droits fondamentaux, la liberté, l'égalité, la justice sociale
|
||||
- L'idée propose des améliorations concrètes pour la société
|
||||
- L'idée est constructive, même si critique du gouvernement ou des institutions
|
||||
- L'idée débat de politiques publiques de manière civile
|
||||
|
||||
Critères de REJET :
|
||||
- Contenu fasciste, totalitaire ou autoritaire
|
||||
- Appels à la haine, discrimination ou violence
|
||||
- Négation de droits fondamentaux pour des groupes de personnes
|
||||
- Propagande pour des idéologies qui détruisent la démocratie
|
||||
- Contenu raciste, sexiste, homophobe ou xénophobe
|
||||
- Appels au renversement violent de la démocratie
|
||||
|
||||
Réponds UNIQUEMENT avec un JSON valide, sans markdown, dans ce format exact :
|
||||
{"accepted": true} ou {"accepted": false, "reason": "explication courte en français"}`;
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-5-mini",
|
||||
max_completion_tokens: 200,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: `Idée à analyser : "${content}"` },
|
||||
],
|
||||
});
|
||||
|
||||
const raw = response.choices[0]?.message?.content ?? '{"accepted": false, "reason": "Erreur d\'analyse"}';
|
||||
const result = JSON.parse(raw) as FilterResult;
|
||||
return result;
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Error filtering idea");
|
||||
return { accepted: false, reason: "Erreur interne de filtrage" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function synthesizeIdeas(ideas: string[]): Promise<string> {
|
||||
if (ideas.length === 0) {
|
||||
return "Aucune idée n'a encore été soumise. Soyez le premier à partager votre vision pour une société meilleure.";
|
||||
}
|
||||
|
||||
const systemPrompt = `Tu es un synthétiseur démocratique. Tu reçois une liste d'idées politiques citoyennes filtrées et validées.
|
||||
Ta mission : créer UN texte synthétique, éloquent et inspirant qui capture l'essence collective de ces idées.
|
||||
|
||||
Ce texte est "La Voix du Peuple" — il doit :
|
||||
- Être écrit à la première personne du pluriel (nous, notre, nos)
|
||||
- Capturer les thèmes communs et les aspirations partagées
|
||||
- Être poétique mais concret, inspirant mais ancré dans la réalité
|
||||
- Faire environ 3-5 paragraphes
|
||||
- Commencer par "Nous, le peuple, ..."
|
||||
- Respecter la diversité des idées sans en ignorer aucune
|
||||
- Être rédigé en français
|
||||
|
||||
NE PAS mentionner les idées individuellement, mais les fondre dans une vision collective cohérente.`;
|
||||
|
||||
const ideasText = ideas.map((idea, i) => `${i + 1}. ${idea}`).join("\n");
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-5.2",
|
||||
max_completion_tokens: 1000,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{
|
||||
role: "user",
|
||||
content: `Voici les idées citoyennes à synthétiser :\n\n${ideasText}\n\nRédige "La Voix du Peuple".`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
return response.choices[0]?.message?.content ?? "Synthèse en cours...";
|
||||
} catch (err) {
|
||||
logger.error({ err }, "Error synthesizing ideas");
|
||||
return "La synthèse est temporairement indisponible. Vos idées ont été enregistrées.";
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import pino from "pino";
|
||||
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
export const logger = pino({
|
||||
level: process.env.LOG_LEVEL ?? "info",
|
||||
redact: [
|
||||
"req.headers.authorization",
|
||||
"req.headers.cookie",
|
||||
"res.headers['set-cookie']",
|
||||
],
|
||||
...(isProduction
|
||||
? {}
|
||||
: {
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
options: { colorize: true },
|
||||
},
|
||||
}),
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { HealthCheckResponse } from "@workspace/api-zod";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/healthz", (_req, res) => {
|
||||
const data = HealthCheckResponse.parse({ status: "ok" });
|
||||
res.json(data);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { eq, count, and } from "drizzle-orm";
|
||||
import { db, ideasTable, synthesisTable } from "@workspace/db";
|
||||
import { SubmitIdeaBody } from "@workspace/api-zod";
|
||||
import { filterIdea, synthesizeIdeas } from "../lib/ai-agent";
|
||||
import { logger } from "../lib/logger";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/ideas", async (_req, res): Promise<void> => {
|
||||
const ideas = await db
|
||||
.select()
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, true))
|
||||
.orderBy(ideasTable.createdAt);
|
||||
res.json(ideas);
|
||||
});
|
||||
|
||||
router.get("/ideas/stats", async (_req, res): Promise<void> => {
|
||||
const [totalRow] = await db.select({ value: count() }).from(ideasTable);
|
||||
const [acceptedRow] = await db
|
||||
.select({ value: count() })
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, true));
|
||||
const [rejectedRow] = await db
|
||||
.select({ value: count() })
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, false));
|
||||
|
||||
res.json({
|
||||
total: Number(totalRow?.value ?? 0),
|
||||
accepted: Number(acceptedRow?.value ?? 0),
|
||||
rejected: Number(rejectedRow?.value ?? 0),
|
||||
});
|
||||
});
|
||||
|
||||
router.post("/ideas", async (req, res): Promise<void> => {
|
||||
const parsed = SubmitIdeaBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: "validation_error", message: parsed.error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const { content, author } = parsed.data;
|
||||
|
||||
req.log.info({ contentLength: content.length }, "Filtering new idea");
|
||||
|
||||
const filterResult = await filterIdea(content);
|
||||
|
||||
const [idea] = await db
|
||||
.insert(ideasTable)
|
||||
.values({
|
||||
content,
|
||||
author: author ?? null,
|
||||
accepted: filterResult.accepted,
|
||||
rejectionReason: filterResult.reason ?? null,
|
||||
})
|
||||
.returning();
|
||||
|
||||
if (filterResult.accepted) {
|
||||
triggerSynthesisUpdate().catch((err) => {
|
||||
logger.error({ err }, "Background synthesis update failed");
|
||||
});
|
||||
}
|
||||
|
||||
res.status(201).json({
|
||||
id: idea!.id,
|
||||
accepted: filterResult.accepted,
|
||||
reason: filterResult.reason,
|
||||
idea: idea,
|
||||
});
|
||||
});
|
||||
|
||||
async function triggerSynthesisUpdate(): Promise<void> {
|
||||
const ideas = await db
|
||||
.select({ content: ideasTable.content })
|
||||
.from(ideasTable)
|
||||
.where(eq(ideasTable.accepted, true))
|
||||
.orderBy(ideasTable.createdAt);
|
||||
|
||||
const ideaTexts = ideas.map((i) => i.content);
|
||||
const synthesizedText = await synthesizeIdeas(ideaTexts);
|
||||
|
||||
const existing = await db.select().from(synthesisTable).limit(1);
|
||||
|
||||
if (existing.length > 0) {
|
||||
await db
|
||||
.update(synthesisTable)
|
||||
.set({ text: synthesizedText, ideaCount: ideaTexts.length });
|
||||
} else {
|
||||
await db.insert(synthesisTable).values({
|
||||
text: synthesizedText,
|
||||
ideaCount: ideaTexts.length,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info({ ideaCount: ideaTexts.length }, "Synthesis updated");
|
||||
}
|
||||
|
||||
export default router;
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import healthRouter from "./health";
|
||||
import ideasRouter from "./ideas";
|
||||
import synthesisRouter from "./synthesis";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.use(healthRouter);
|
||||
router.use(ideasRouter);
|
||||
router.use(synthesisRouter);
|
||||
|
||||
export default router;
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Router, type IRouter } from "express";
|
||||
import { db, synthesisTable } from "@workspace/db";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
router.get("/synthesis", async (_req, res): Promise<void> => {
|
||||
const [synthesis] = await db.select().from(synthesisTable).limit(1);
|
||||
|
||||
if (!synthesis) {
|
||||
res.json({
|
||||
text: "Aucune idée n'a encore été soumise. Soyez le premier à partager votre vision pour une société meilleure.",
|
||||
ideaCount: 0,
|
||||
updatedAt: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({
|
||||
text: synthesis.text,
|
||||
ideaCount: synthesis.ideaCount,
|
||||
updatedAt: synthesis.updatedAt,
|
||||
});
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../lib/db"
|
||||
},
|
||||
{
|
||||
"path": "../../lib/api-zod"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ La Voix du Peuple — Agent IA
|
||||
Copyright (C) 2026 billisdead — Licence EUPL-1.2
|
||||
|
||||
Agent IA pour le filtrage éthique et la synthèse démocratique.
|
||||
Supporte Mistral AI, OpenAI, et les intégrations Replit AI.
|
||||
Supporte Mistral AI (par défaut) et tout fournisseur compatible OpenAI.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
@@ -20,10 +20,9 @@ _client: OpenAI | None = None
|
||||
|
||||
def get_client() -> OpenAI:
|
||||
"""
|
||||
Supporte trois modes (par ordre de priorité) :
|
||||
1. Mistral AI : MISTRAL_API_KEY (+ MISTRAL_BASE_URL optionnel)
|
||||
2. OpenAI standard : OPENAI_API_KEY (+ OPENAI_BASE_URL optionnel)
|
||||
3. Replit AI Integration : AI_INTEGRATIONS_OPENAI_BASE_URL + AI_INTEGRATIONS_OPENAI_API_KEY
|
||||
Supporte deux modes (par ordre de priorité) :
|
||||
1. Mistral AI : MISTRAL_API_KEY (+ MISTRAL_BASE_URL optionnel)
|
||||
2. OpenAI-compatible : OPENAI_API_KEY (+ OPENAI_BASE_URL optionnel)
|
||||
"""
|
||||
global _client
|
||||
if _client is None:
|
||||
@@ -31,26 +30,20 @@ def get_client() -> OpenAI:
|
||||
mistral_base = os.environ.get("MISTRAL_BASE_URL", MISTRAL_BASE_URL)
|
||||
std_key = os.environ.get("OPENAI_API_KEY")
|
||||
std_base = os.environ.get("OPENAI_BASE_URL")
|
||||
replit_base = os.environ.get("AI_INTEGRATIONS_OPENAI_BASE_URL")
|
||||
replit_key = os.environ.get("AI_INTEGRATIONS_OPENAI_API_KEY")
|
||||
|
||||
if mistral_key:
|
||||
logger.info("Utilisation de l'API Mistral AI (%s)", mistral_base)
|
||||
_client = OpenAI(base_url=mistral_base, api_key=mistral_key)
|
||||
elif std_key:
|
||||
logger.info("Utilisation de l'API OpenAI")
|
||||
kwargs = {"api_key": std_key}
|
||||
logger.info("Utilisation d'une API compatible OpenAI")
|
||||
kwargs: dict = {"api_key": std_key}
|
||||
if std_base:
|
||||
kwargs["base_url"] = std_base
|
||||
_client = OpenAI(**kwargs)
|
||||
elif replit_base and replit_key:
|
||||
logger.info("Utilisation de l'intégration Replit AI")
|
||||
_client = OpenAI(base_url=replit_base, api_key=replit_key)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Aucune clé IA configurée. "
|
||||
"Définissez MISTRAL_API_KEY ou OPENAI_API_KEY dans le fichier .env, "
|
||||
"ou configurez les intégrations Replit AI."
|
||||
"Définissez MISTRAL_API_KEY (recommandé) ou OPENAI_API_KEY dans le fichier .env."
|
||||
)
|
||||
return _client
|
||||
|
||||
|
||||
@@ -38,9 +38,6 @@
|
||||
"@radix-ui/react-toggle": "^1.1.3",
|
||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "^1.2.0",
|
||||
"@replit/vite-plugin-cartographer": "catalog:",
|
||||
"@replit/vite-plugin-dev-banner": "catalog:",
|
||||
"@replit/vite-plugin-runtime-error-modal": "catalog:",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@tailwindcss/vite": "catalog:",
|
||||
"@tanstack/react-query": "catalog:",
|
||||
|
||||
@@ -4,23 +4,17 @@ import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
// @replit
|
||||
// Whitespace-nowrap: Badges should never wrap.
|
||||
"whitespace-nowrap inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" +
|
||||
" hover-elevate ",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
// @replit shadow-xs instead of shadow, no hover because we use hover-elevate
|
||||
"border-transparent bg-primary text-primary-foreground shadow-xs",
|
||||
secondary:
|
||||
// @replit no hover because we use hover-elevate
|
||||
"border-transparent bg-secondary text-secondary-foreground",
|
||||
destructive:
|
||||
// @replit shadow-xs instead of shadow, no hover because we use hover-elevate
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow-xs",
|
||||
// @replit shadow-xs" - use badge outline variable
|
||||
outline: "text-foreground border [border-color:var(--badge-outline)]",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,24 +11,17 @@ const buttonVariants = cva(
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
// @replit: no hover, and add primary border
|
||||
"bg-primary text-primary-foreground border border-primary-border",
|
||||
"bg-primary text-primary-foreground border border-primary-border",
|
||||
destructive:
|
||||
"bg-destructive text-destructive-foreground shadow-sm border-destructive-border",
|
||||
outline:
|
||||
// @replit Shows the background color of whatever card / sidebar / accent background it is inside of.
|
||||
// Inherits the current text color. Uses shadow-xs. no shadow on active
|
||||
// No hover state
|
||||
" border [border-color:var(--button-outline)] shadow-xs active:shadow-none ",
|
||||
secondary:
|
||||
// @replit border, no hover, no shadow, secondary border.
|
||||
"border bg-secondary text-secondary-foreground border border-secondary-border ",
|
||||
// @replit no hover, transparent border
|
||||
ghost: "border border-transparent",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
// @replit changed sizes
|
||||
default: "min-h-9 px-4 py-2",
|
||||
sm: "min-h-8 rounded-md px-3 text-xs",
|
||||
lg: "min-h-10 rounded-md px-8",
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
base: "/",
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(import.meta.dirname, "src"),
|
||||
},
|
||||
dedupe: ["react", "react-dom"],
|
||||
},
|
||||
root: path.resolve(import.meta.dirname),
|
||||
build: {
|
||||
outDir: path.resolve(import.meta.dirname, "dist/public"),
|
||||
emptyOutDir: true,
|
||||
},
|
||||
});
|
||||
@@ -2,49 +2,15 @@ import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import path from "path";
|
||||
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
||||
|
||||
const rawPort = process.env.PORT;
|
||||
|
||||
if (!rawPort) {
|
||||
throw new Error(
|
||||
"PORT environment variable is required but was not provided.",
|
||||
);
|
||||
}
|
||||
|
||||
const port = Number(rawPort);
|
||||
|
||||
if (Number.isNaN(port) || port <= 0) {
|
||||
throw new Error(`Invalid PORT value: "${rawPort}"`);
|
||||
}
|
||||
|
||||
const basePath = process.env.BASE_PATH;
|
||||
|
||||
if (!basePath) {
|
||||
throw new Error(
|
||||
"BASE_PATH environment variable is required but was not provided.",
|
||||
);
|
||||
}
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : 5173;
|
||||
const basePath = process.env.BASE_PATH ?? "/";
|
||||
|
||||
export default defineConfig({
|
||||
base: basePath,
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss(),
|
||||
runtimeErrorOverlay(),
|
||||
...(process.env.NODE_ENV !== "production" &&
|
||||
process.env.REPL_ID !== undefined
|
||||
? [
|
||||
await import("@replit/vite-plugin-cartographer").then((m) =>
|
||||
m.cartographer({
|
||||
root: path.resolve(import.meta.dirname, ".."),
|
||||
}),
|
||||
),
|
||||
await import("@replit/vite-plugin-dev-banner").then((m) =>
|
||||
m.devBanner(),
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
+5
-6
@@ -2,7 +2,7 @@
|
||||
|
||||
**Version** : 1.4
|
||||
**Date** : Avril 2026
|
||||
**Statut** : En production (Replit), prêt pour auto-hébergement
|
||||
**Statut** : Prêt pour auto-hébergement
|
||||
|
||||
---
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
|
||||
**Variables d'environnement frontend** :
|
||||
- `BASE_URL` — Préfixe de chemin (injecté par Vite)
|
||||
- `PORT` — Port du serveur de développement (assigné par Replit)
|
||||
- `PORT` — Port du serveur de développement (défaut : 5173)
|
||||
|
||||
---
|
||||
|
||||
@@ -172,9 +172,8 @@ Deux appels distincts à l'API Mistral (compatible OpenAI SDK) :
|
||||
- **Déclencheur** : À chaque nouvelle contribution acceptée (asynchrone)
|
||||
|
||||
**Priorité de configuration du client IA** :
|
||||
1. `MISTRAL_API_KEY` → `https://api.mistral.ai/v1`
|
||||
2. `OPENAI_API_KEY` → API OpenAI standard
|
||||
3. `AI_INTEGRATIONS_OPENAI_*` → Proxy Replit (intégration native)
|
||||
1. `MISTRAL_API_KEY` → `https://api.mistral.ai/v1` (recommandé)
|
||||
2. `OPENAI_API_KEY` → tout fournisseur compatible OpenAI (`OPENAI_BASE_URL` optionnel)
|
||||
|
||||
---
|
||||
|
||||
@@ -232,7 +231,7 @@ Internet ──▶ HAProxy (TLS, load balancing)
|
||||
**Fichiers fournis** :
|
||||
- `deploy/nginx.conf` — Configuration Nginx avec HAProxy support
|
||||
- `deploy/voix-du-peuple-api.service` — Unité systemd pour Gunicorn
|
||||
- `artifacts/voix-du-peuple/vite.config.selfhost.ts` — Build sans plugins Replit
|
||||
- `artifacts/voix-du-peuple/vite.config.ts` — Config Vite (développement + production)
|
||||
- `scripts/push-gitea.sh` — Push sécurisé vers Gitea (compatible Git 2.50+, lit `GITEA_TOKEN`)
|
||||
|
||||
---
|
||||
|
||||
+5
-26
@@ -21,19 +21,6 @@
|
||||
|
||||
## 1. Démarrage et arrêt des services
|
||||
|
||||
### Sur Replit
|
||||
|
||||
Les services sont gérés par les **Workflows** Replit. Ils démarrent automatiquement.
|
||||
|
||||
| Workflow | Commande | Port |
|
||||
|----------|----------|------|
|
||||
| API Server | `PORT=8080 sh artifacts/flask-api/start.sh` | 8080 |
|
||||
| Frontend | `pnpm --filter @workspace/voix-du-peuple run dev` | auto |
|
||||
|
||||
Pour redémarrer manuellement : onglet **Workflows** → bouton restart.
|
||||
|
||||
---
|
||||
|
||||
### En auto-hébergement (RockyLinux / Debian)
|
||||
|
||||
```bash
|
||||
@@ -111,10 +98,6 @@ systemctl restart voix-du-peuple-api
|
||||
|
||||
## 4. Consultation des logs
|
||||
|
||||
### Sur Replit
|
||||
|
||||
Voir les logs dans l'onglet **Workflows** → cliquer sur le workflow concerné.
|
||||
|
||||
### En auto-hébergement
|
||||
|
||||
```bash
|
||||
@@ -352,19 +335,15 @@ Toutes les occurrences de `--primary` dans le fichier CSS s'appliquent automatiq
|
||||
|
||||
---
|
||||
|
||||
## 15. Synchronisation avec Gitea
|
||||
|
||||
Pour pousser le code depuis Replit vers Gitea (après chaque session de travail) :
|
||||
## 15. Push vers Gitea
|
||||
|
||||
```bash
|
||||
bash scripts/push-gitea.sh
|
||||
git push
|
||||
```
|
||||
|
||||
**Prérequis** : le secret `GITEA_TOKEN` doit être configuré dans Replit → Secrets.
|
||||
> **Note** : le script `scripts/push-gitea.sh` permet un push avec token HTTP si besoin (`GITEA_TOKEN`). Pour Git 2.50+, préférer `git remote set-url` avec un token en variable d'environnement.
|
||||
|
||||
> **Contexte** : Git 2.50+ ignore les tokens embarqués dans les URLs. Le script contourne ce comportement en transmettant les identifiants via l'en-tête HTTP `Authorization: Basic` (encodage Base64). Voir `docs/GITEA_TUTO.md` pour le détail complet.
|
||||
|
||||
Pour régénérer un token Gitea : **Paramètres du compte Gitea → Applications → Générer un token**
|
||||
Pour régénérer un token Gitea : **Paramètres du compte → Applications → Générer un token**
|
||||
Permissions requises : `repository` (lecture + écriture).
|
||||
|
||||
---
|
||||
@@ -385,7 +364,7 @@ https://votredomaine.fr/admin
|
||||
|
||||
| Variable | Où la définir |
|
||||
|----------|---------------|
|
||||
| `ADMIN_SECRET` | Replit → Secrets · ou `.env` en auto-hébergement |
|
||||
| `ADMIN_SECRET` | `.env` ou variable d'environnement système |
|
||||
|
||||
### Fonctionnalités
|
||||
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
# Récupérer le projet sur Gitea
|
||||
|
||||
Ce tutoriel explique comment pousser le code de **La Voix du Peuple** depuis Replit vers votre instance Gitea, et comment vous synchroniser ensuite à votre rythme.
|
||||
|
||||
> **État du dépôt** : branche `main`, synchronisée. Remote actif : `origin` (Gitea).
|
||||
|
||||
---
|
||||
|
||||
## Prérequis
|
||||
|
||||
- Une instance Gitea accessible (`https://homegit.gyozamancave.fr`)
|
||||
- Un token d'accès Gitea avec droits `repository:write`
|
||||
- Le secret `GITEA_TOKEN` configuré dans Replit (voir Étape 1)
|
||||
|
||||
---
|
||||
|
||||
## Étape 1 — Stocker le token dans Replit Secrets
|
||||
|
||||
Votre token Gitea doit être stocké comme secret Replit, jamais dans un fichier versionné.
|
||||
|
||||
1. Dans Replit → **Secrets** (cadenas dans le panneau latéral)
|
||||
2. Ajouter un secret :
|
||||
- **Clé** : `GITEA_TOKEN`
|
||||
- **Valeur** : votre token Gitea
|
||||
|
||||
Pour générer ou renouveler un token sur Gitea :
|
||||
**Paramètres du compte → Applications → Générer un token**
|
||||
Permissions requises : `repository` (lecture + écriture)
|
||||
|
||||
---
|
||||
|
||||
## Étape 2 — Pousser vers Gitea
|
||||
|
||||
Le script `scripts/push-gitea.sh` gère l'authentification automatiquement.
|
||||
|
||||
Dans le shell Replit :
|
||||
|
||||
```bash
|
||||
bash scripts/push-gitea.sh
|
||||
```
|
||||
|
||||
Ce script :
|
||||
- Lit `GITEA_TOKEN` depuis l'environnement
|
||||
- Encode les identifiants en Base64 (compatibilité Git 2.50+)
|
||||
- Pousse la branche `main` vers `origin` (Gitea)
|
||||
|
||||
---
|
||||
|
||||
## Pourquoi ce script ? (contexte technique)
|
||||
|
||||
Git 2.50+ ignore les tokens embarqués dans les URLs (`https://user:token@host/...`) pour des raisons de sécurité. La solution est de passer l'en-tête `Authorization: Basic` explicitement via `-c http.extraHeader`.
|
||||
|
||||
```bash
|
||||
# Ce que fait le script, en détail :
|
||||
B64=$(printf '%s' "billisdead:${GITEA_TOKEN}" | base64 -w0)
|
||||
git -c "http.extraHeader=Authorization: Basic ${B64}" push origin main
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 3 — Récupérer les mises à jour depuis Replit
|
||||
|
||||
Chaque fois que vous voulez synchroniser votre Gitea avec l'état actuel du projet :
|
||||
|
||||
```bash
|
||||
bash scripts/push-gitea.sh
|
||||
```
|
||||
|
||||
Si vous avez modifié des fichiers directement sur Gitea (peu recommandé) :
|
||||
|
||||
```bash
|
||||
# Récupérer les changements Gitea d'abord
|
||||
B64=$(printf '%s' "billisdead:${GITEA_TOKEN}" | base64 -w0)
|
||||
git -c "http.extraHeader=Authorization: Basic ${B64}" pull origin main --rebase
|
||||
bash scripts/push-gitea.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étape 4 — Cloner depuis Gitea sur votre serveur de production
|
||||
|
||||
Sur votre serveur RockyLinux / Debian :
|
||||
|
||||
```bash
|
||||
git clone https://homegit.gyozamancave.fr/billisdead/la-voix-du-peuple.git
|
||||
cd la-voix-du-peuple
|
||||
|
||||
# Copier et adapter les variables d'environnement
|
||||
cp .env.example .env
|
||||
nano .env # Renseignez DATABASE_URL, MISTRAL_API_KEY, SESSION_SECRET
|
||||
|
||||
# Installer les dépendances Python
|
||||
pip install -r artifacts/flask-api/requirements.txt
|
||||
|
||||
# Installer les dépendances Node
|
||||
pnpm install
|
||||
|
||||
# Construire le frontend
|
||||
pnpm --filter @workspace/voix-du-peuple run build --config vite.config.selfhost.ts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Résumé des commandes utiles
|
||||
|
||||
```bash
|
||||
# Pousser vers Gitea (commande principale)
|
||||
bash scripts/push-gitea.sh
|
||||
|
||||
# Voir l'état du dépôt
|
||||
git status
|
||||
git log --oneline -10
|
||||
|
||||
# Voir les remotes configurés
|
||||
git remote -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Structure des branches
|
||||
|
||||
| Branche | Usage |
|
||||
|---------|-------|
|
||||
| `main` | Code de production, stable — **seule branche à pousser vers Gitea** |
|
||||
| `replit-agent` | Branche de travail interne de l'agent Replit — ne pas pousser |
|
||||
|
||||
---
|
||||
|
||||
## Remotes configurés
|
||||
|
||||
| Nom | URL | Usage |
|
||||
|-----|-----|-------|
|
||||
| `origin` | `https://homegit.gyozamancave.fr/billisdead/la-voix-du-peuple.git` | Gitea principal |
|
||||
| `gitsafe-backup` | `git://gitsafe:5418/backup.git` | Sauvegarde interne Replit |
|
||||
|
||||
> **Important** : ne jamais inclure de token dans une URL de remote. Le token doit rester dans `GITEA_TOKEN` (Replit Secrets) et être transmis via le script.
|
||||
|
||||
---
|
||||
|
||||
## Contenu du dépôt (structure principale)
|
||||
|
||||
```
|
||||
artifacts/
|
||||
flask-api/ ← Backend Python Flask
|
||||
app.py ← Routes API
|
||||
ai_agent.py ← Intégration Mistral / OpenAI
|
||||
database.py ← Accès PostgreSQL
|
||||
requirements.txt ← Dépendances Python
|
||||
voix-du-peuple/ ← Frontend React + Vite
|
||||
src/
|
||||
pages/
|
||||
home.tsx ← Page principale (synthèse, partage, PDF)
|
||||
about.tsx ← À propos
|
||||
transparence.tsx ← Fonctionnement & données
|
||||
flyer.tsx ← Flyer QR code imprimable
|
||||
components/
|
||||
accessibility-panel.tsx ← Panneau d'accessibilité
|
||||
hooks/
|
||||
use-accessibility.tsx ← Contexte et persistance
|
||||
App.tsx ← Routing et navbar
|
||||
index.css ← Styles globaux, dark mode, accessibilité
|
||||
scripts/
|
||||
push-gitea.sh ← Script de push sécurisé vers Gitea
|
||||
deploy/
|
||||
nginx.conf ← Config Nginx production
|
||||
voix-du-peuple-api.service ← Unité systemd Gunicorn
|
||||
docs/
|
||||
DAT.md ← Architecture technique
|
||||
DEX.md ← Exploitation
|
||||
WIKI.md ← Page wiki
|
||||
GITEA_TUTO.md ← Ce fichier
|
||||
```
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Type** : Plateforme civique numérique
|
||||
**Stack** : Python / Flask · React / Vite · PostgreSQL · Mistral AI
|
||||
**Hébergement** : Replit (dev) / Auto-hébergeable (RockyLinux, Debian)
|
||||
**Hébergement** : Auto-hébergeable (RockyLinux, Debian, VPS UE)
|
||||
**Dépôt** : `voix-du-peuple` (Gitea)
|
||||
**Statut** : Actif — avril 2026
|
||||
**Version doc** : 1.4
|
||||
|
||||
+2
-12
@@ -16,7 +16,7 @@
|
||||
# passed (e.g. an urgent security bugfix), you can add it to the
|
||||
# `minimumReleaseAgeExclude` allowlist below. Only consider doing this for
|
||||
# packages released by trusted organizations with an impeccable security
|
||||
# posture (e.g. Replit packsges, react from Meta, typescript from Microsoft). Even then,
|
||||
# posture (e.g. react from Meta, typescript from Microsoft). Even then,
|
||||
# remove the exclusion once the 1-day window has passed.
|
||||
#
|
||||
# Example:
|
||||
@@ -27,13 +27,6 @@
|
||||
# ============================================================================
|
||||
minimumReleaseAge: 1440
|
||||
|
||||
minimumReleaseAgeExclude:
|
||||
# Exclude @replit scoped packages from the minimum release age check.
|
||||
# These are published by Replit and trusted — the supply-chain attack vector
|
||||
# this setting guards against does not apply to our own packages.
|
||||
- '@replit/*'
|
||||
- stripe-replit-sync
|
||||
|
||||
packages:
|
||||
- artifacts/*
|
||||
- lib/*
|
||||
@@ -41,9 +34,6 @@ packages:
|
||||
- scripts
|
||||
|
||||
catalog:
|
||||
'@replit/vite-plugin-cartographer': ^0.5.1
|
||||
'@replit/vite-plugin-dev-banner': ^0.1.1
|
||||
'@replit/vite-plugin-runtime-error-modal': ^0.0.6
|
||||
'@tailwindcss/vite': ^4.1.14
|
||||
'@tanstack/react-query': ^5.90.21
|
||||
'@types/node': ^25.3.3
|
||||
@@ -74,7 +64,7 @@ onlyBuiltDependencies:
|
||||
- unrs-resolver
|
||||
|
||||
overrides:
|
||||
# replit uses linux-x64 only, we can exclude all other platforms
|
||||
# Déploiement Linux x64 uniquement — on exclut les binaires inutiles
|
||||
"esbuild>@esbuild/darwin-arm64": "-"
|
||||
"esbuild>@esbuild/darwin-x64": "-"
|
||||
"esbuild>@esbuild/freebsd-arm64": "-"
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
# La Voix du Peuple
|
||||
|
||||
## Vue d'ensemble
|
||||
|
||||
Plateforme démocratique citoyenne où les citoyens soumettent leurs idées politiques, filtrées par IA selon le droit international des droits humains, puis synthétisées en un texte collectif vivant.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Frontend
|
||||
- **React + Vite** (`artifacts/voix-du-peuple/`)
|
||||
- Interface bicolonne : formulaire de soumission + synthèse en direct
|
||||
- Auto-refresh de la synthèse toutes les 15 secondes
|
||||
- Hooks générés par Orval depuis l'OpenAPI spec
|
||||
|
||||
### Backend
|
||||
- **Python Flask** (`artifacts/flask-api/`) — remplace le serveur TypeScript/Express
|
||||
- Sert l'API sur `/api/*`
|
||||
- Démarre via `artifacts/flask-api/start.sh`
|
||||
|
||||
### IA Agentique (deux agents)
|
||||
1. **Agent filtrage** (`ai_agent.py::filter_idea`) — gpt-5-mini
|
||||
- Filtre selon DUDH, PIDCP, CEDH, Charte UE, etc.
|
||||
- Double protection : filtre du proxy Azure + filtre légal IA
|
||||
2. **Agent synthèse** (`ai_agent.py::synthesize_ideas`) — gpt-5.2
|
||||
- Synthèse collective en français, "Nous, le peuple..."
|
||||
- Déclenché en arrière-plan à chaque nouvelle idée acceptée
|
||||
|
||||
### Base légale du filtre (`legal_framework.py`)
|
||||
- Déclaration universelle des droits de l'homme (DUDH, ONU 1948)
|
||||
- Pacte international relatif aux droits civils et politiques (PIDCP, ONU 1966)
|
||||
- Convention européenne des droits de l'homme (CEDH, 1950)
|
||||
- Charte des droits fondamentaux de l'UE (2000/2009)
|
||||
- Convention pour la prévention du génocide (ONU 1948)
|
||||
- Statut de Rome / CPI (1998)
|
||||
- CERD — Convention sur la discrimination raciale (ONU 1965)
|
||||
|
||||
### Base de données
|
||||
- **PostgreSQL** via `psycopg2` directement (pas d'ORM Flask)
|
||||
- Tables : `ideas`, `synthesis`
|
||||
- Drizzle ORM maintenu côté TypeScript pour compatibilité (schema dans `lib/db/`)
|
||||
|
||||
## Stack
|
||||
|
||||
- **Monorepo** : pnpm workspaces
|
||||
- **Node.js** : 24 (pour le frontend React)
|
||||
- **Python** : 3.11 (pour le backend Flask)
|
||||
- **TypeScript** : 5.9
|
||||
- **API** : OpenAPI spec → Orval codegen → React Query hooks
|
||||
- **IA** : Replit AI Integrations (OpenAI proxy, pas de clé API requise)
|
||||
|
||||
## Sécurité Flask
|
||||
- Rate limiting : 5 soumissions/minute, 20/heure par IP (`flask-limiter`)
|
||||
- Assainissement XSS : `bleach.clean()` sur toutes les entrées
|
||||
- En-têtes HTTP : CSP, X-Frame-Options DENY, X-Content-Type-Options, etc.
|
||||
- Requêtes paramétrées : `psycopg2` avec `%s` — protection injection SQL
|
||||
- CORS configuré
|
||||
- Aucun secret exposé dans les réponses d'erreur
|
||||
|
||||
## Commandes clés
|
||||
|
||||
- `pnpm --filter @workspace/api-spec run codegen` — régénérer les hooks React
|
||||
- `pnpm --filter @workspace/db run push` — migrer le schéma DB
|
||||
- `pnpm --filter @workspace/voix-du-peuple run dev` — frontend dev
|
||||
- `sh artifacts/flask-api/start.sh` — backend Flask
|
||||
|
||||
## Workflows
|
||||
|
||||
- `artifacts/api-server: API Server` → Flask (port 8080, chemin `/api`)
|
||||
- `artifacts/voix-du-peuple: web` → React/Vite (port 20108, chemin `/`)
|
||||
Reference in New Issue
Block a user