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
|
- **Frontend** : React + Vite, servi comme fichiers statiques par Nginx
|
||||||
- **Backend** : Flask + Gunicorn (4 workers), accessible uniquement via Nginx
|
- **Backend** : Flask + Gunicorn (4 workers), accessible uniquement via Nginx
|
||||||
- **Base de données** : PostgreSQL 15+
|
- **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
|
│ │ ├── pages/ # home.tsx, about.tsx
|
||||||
│ │ ├── components/ # Composants UI (shadcn/ui + radix)
|
│ │ ├── components/ # Composants UI (shadcn/ui + radix)
|
||||||
│ │ └── App.tsx # Routing principal
|
│ │ └── App.tsx # Routing principal
|
||||||
│ ├── vite.config.ts # Config Replit (développement)
|
│ └── vite.config.ts # Config Vite (développement + production)
|
||||||
│ └── vite.config.selfhost.ts # Config auto-hébergement (production)
|
|
||||||
├── lib/
|
├── lib/
|
||||||
│ ├── api-spec/ # Spécification OpenAPI
|
│ ├── api-spec/ # Spécification OpenAPI
|
||||||
│ ├── api-client-react/ # Hooks React Query générés
|
│ ├── api-client-react/ # Hooks React Query générés
|
||||||
@@ -327,12 +326,10 @@ voix-du-peuple/
|
|||||||
|
|
||||||
## Modèles IA utilisés
|
## Modèles IA utilisés
|
||||||
|
|
||||||
| Fonction | Modèle par défaut | Configurable dans |
|
| Fonction | Modèle par défaut | Variable d'environnement |
|
||||||
|----------|-------------------|-------------------|
|
|----------|-------------------|--------------------------|
|
||||||
| Filtrage des idées | `gpt-4o-mini` | `ai_agent.py` ligne 56 |
|
| Filtrage des idées | `mistral-small-latest` | `FILTER_MODEL` |
|
||||||
| Synthèse collective | `gpt-4o` | `ai_agent.py` ligne 104 |
|
| Synthèse collective | `mistral-large-latest` | `SYNTHESIS_MODEL` |
|
||||||
|
|
||||||
> **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`.
|
|
||||||
|
|
||||||
Pour changer les modèles, éditez `artifacts/flask-api/ai_agent.py` :
|
Pour changer les modèles, éditez `artifacts/flask-api/ai_agent.py` :
|
||||||
```python
|
```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
|
## Documentation
|
||||||
|
|
||||||
| Document | Contenu |
|
| 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/DEX.md`](docs/DEX.md) | Guide d'exploitation et maintenance |
|
||||||
| [`docs/WIKI.md`](docs/WIKI.md) | Page wiki — présentation générale |
|
| [`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/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/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
|
Copyright (C) 2026 billisdead — Licence EUPL-1.2
|
||||||
|
|
||||||
Agent IA pour le filtrage éthique et la synthèse démocratique.
|
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 json
|
||||||
import os
|
import os
|
||||||
@@ -20,10 +20,9 @@ _client: OpenAI | None = None
|
|||||||
|
|
||||||
def get_client() -> OpenAI:
|
def get_client() -> OpenAI:
|
||||||
"""
|
"""
|
||||||
Supporte trois modes (par ordre de priorité) :
|
Supporte deux modes (par ordre de priorité) :
|
||||||
1. Mistral AI : MISTRAL_API_KEY (+ MISTRAL_BASE_URL optionnel)
|
1. Mistral AI : MISTRAL_API_KEY (+ MISTRAL_BASE_URL optionnel)
|
||||||
2. OpenAI standard : OPENAI_API_KEY (+ OPENAI_BASE_URL optionnel)
|
2. OpenAI-compatible : OPENAI_API_KEY (+ OPENAI_BASE_URL optionnel)
|
||||||
3. Replit AI Integration : AI_INTEGRATIONS_OPENAI_BASE_URL + AI_INTEGRATIONS_OPENAI_API_KEY
|
|
||||||
"""
|
"""
|
||||||
global _client
|
global _client
|
||||||
if _client is None:
|
if _client is None:
|
||||||
@@ -31,26 +30,20 @@ def get_client() -> OpenAI:
|
|||||||
mistral_base = os.environ.get("MISTRAL_BASE_URL", MISTRAL_BASE_URL)
|
mistral_base = os.environ.get("MISTRAL_BASE_URL", MISTRAL_BASE_URL)
|
||||||
std_key = os.environ.get("OPENAI_API_KEY")
|
std_key = os.environ.get("OPENAI_API_KEY")
|
||||||
std_base = os.environ.get("OPENAI_BASE_URL")
|
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:
|
if mistral_key:
|
||||||
logger.info("Utilisation de l'API Mistral AI (%s)", mistral_base)
|
logger.info("Utilisation de l'API Mistral AI (%s)", mistral_base)
|
||||||
_client = OpenAI(base_url=mistral_base, api_key=mistral_key)
|
_client = OpenAI(base_url=mistral_base, api_key=mistral_key)
|
||||||
elif std_key:
|
elif std_key:
|
||||||
logger.info("Utilisation de l'API OpenAI")
|
logger.info("Utilisation d'une API compatible OpenAI")
|
||||||
kwargs = {"api_key": std_key}
|
kwargs: dict = {"api_key": std_key}
|
||||||
if std_base:
|
if std_base:
|
||||||
kwargs["base_url"] = std_base
|
kwargs["base_url"] = std_base
|
||||||
_client = OpenAI(**kwargs)
|
_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:
|
else:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Aucune clé IA configurée. "
|
"Aucune clé IA configurée. "
|
||||||
"Définissez MISTRAL_API_KEY ou OPENAI_API_KEY dans le fichier .env, "
|
"Définissez MISTRAL_API_KEY (recommandé) ou OPENAI_API_KEY dans le fichier .env."
|
||||||
"ou configurez les intégrations Replit AI."
|
|
||||||
)
|
)
|
||||||
return _client
|
return _client
|
||||||
|
|
||||||
|
|||||||
@@ -38,9 +38,6 @@
|
|||||||
"@radix-ui/react-toggle": "^1.1.3",
|
"@radix-ui/react-toggle": "^1.1.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.1.3",
|
"@radix-ui/react-toggle-group": "^1.1.3",
|
||||||
"@radix-ui/react-tooltip": "^1.2.0",
|
"@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/typography": "^0.5.15",
|
||||||
"@tailwindcss/vite": "catalog:",
|
"@tailwindcss/vite": "catalog:",
|
||||||
"@tanstack/react-query": "catalog:",
|
"@tanstack/react-query": "catalog:",
|
||||||
|
|||||||
@@ -4,23 +4,17 @@ import { cva, type VariantProps } from "class-variance-authority"
|
|||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const badgeVariants = cva(
|
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" +
|
"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 ",
|
" hover-elevate ",
|
||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default:
|
||||||
// @replit shadow-xs instead of shadow, no hover because we use hover-elevate
|
|
||||||
"border-transparent bg-primary text-primary-foreground shadow-xs",
|
"border-transparent bg-primary text-primary-foreground shadow-xs",
|
||||||
secondary:
|
secondary:
|
||||||
// @replit no hover because we use hover-elevate
|
|
||||||
"border-transparent bg-secondary text-secondary-foreground",
|
"border-transparent bg-secondary text-secondary-foreground",
|
||||||
destructive:
|
destructive:
|
||||||
// @replit shadow-xs instead of shadow, no hover because we use hover-elevate
|
|
||||||
"border-transparent bg-destructive text-destructive-foreground shadow-xs",
|
"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)]",
|
outline: "text-foreground border [border-color:var(--badge-outline)]",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,24 +11,17 @@ const buttonVariants = cva(
|
|||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
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:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground shadow-sm border-destructive-border",
|
"bg-destructive text-destructive-foreground shadow-sm border-destructive-border",
|
||||||
outline:
|
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 ",
|
" border [border-color:var(--button-outline)] shadow-xs active:shadow-none ",
|
||||||
secondary:
|
secondary:
|
||||||
// @replit border, no hover, no shadow, secondary border.
|
|
||||||
"border bg-secondary text-secondary-foreground border border-secondary-border ",
|
"border bg-secondary text-secondary-foreground border border-secondary-border ",
|
||||||
// @replit no hover, transparent border
|
|
||||||
ghost: "border border-transparent",
|
ghost: "border border-transparent",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
// @replit changed sizes
|
|
||||||
default: "min-h-9 px-4 py-2",
|
default: "min-h-9 px-4 py-2",
|
||||||
sm: "min-h-8 rounded-md px-3 text-xs",
|
sm: "min-h-8 rounded-md px-3 text-xs",
|
||||||
lg: "min-h-10 rounded-md px-8",
|
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 react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import runtimeErrorOverlay from "@replit/vite-plugin-runtime-error-modal";
|
|
||||||
|
|
||||||
const rawPort = process.env.PORT;
|
const port = process.env.PORT ? Number(process.env.PORT) : 5173;
|
||||||
|
const basePath = process.env.BASE_PATH ?? "/";
|
||||||
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.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
base: basePath,
|
base: basePath,
|
||||||
plugins: [
|
plugins: [
|
||||||
react(),
|
react(),
|
||||||
tailwindcss(),
|
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: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
+5
-6
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**Version** : 1.4
|
**Version** : 1.4
|
||||||
**Date** : Avril 2026
|
**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** :
|
**Variables d'environnement frontend** :
|
||||||
- `BASE_URL` — Préfixe de chemin (injecté par Vite)
|
- `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)
|
- **Déclencheur** : À chaque nouvelle contribution acceptée (asynchrone)
|
||||||
|
|
||||||
**Priorité de configuration du client IA** :
|
**Priorité de configuration du client IA** :
|
||||||
1. `MISTRAL_API_KEY` → `https://api.mistral.ai/v1`
|
1. `MISTRAL_API_KEY` → `https://api.mistral.ai/v1` (recommandé)
|
||||||
2. `OPENAI_API_KEY` → API OpenAI standard
|
2. `OPENAI_API_KEY` → tout fournisseur compatible OpenAI (`OPENAI_BASE_URL` optionnel)
|
||||||
3. `AI_INTEGRATIONS_OPENAI_*` → Proxy Replit (intégration native)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -232,7 +231,7 @@ Internet ──▶ HAProxy (TLS, load balancing)
|
|||||||
**Fichiers fournis** :
|
**Fichiers fournis** :
|
||||||
- `deploy/nginx.conf` — Configuration Nginx avec HAProxy support
|
- `deploy/nginx.conf` — Configuration Nginx avec HAProxy support
|
||||||
- `deploy/voix-du-peuple-api.service` — Unité systemd pour Gunicorn
|
- `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`)
|
- `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
|
## 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)
|
### En auto-hébergement (RockyLinux / Debian)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -111,10 +98,6 @@ systemctl restart voix-du-peuple-api
|
|||||||
|
|
||||||
## 4. Consultation des logs
|
## 4. Consultation des logs
|
||||||
|
|
||||||
### Sur Replit
|
|
||||||
|
|
||||||
Voir les logs dans l'onglet **Workflows** → cliquer sur le workflow concerné.
|
|
||||||
|
|
||||||
### En auto-hébergement
|
### En auto-hébergement
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -352,19 +335,15 @@ Toutes les occurrences de `--primary` dans le fichier CSS s'appliquent automatiq
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 15. Synchronisation avec Gitea
|
## 15. Push vers Gitea
|
||||||
|
|
||||||
Pour pousser le code depuis Replit vers Gitea (après chaque session de travail) :
|
|
||||||
|
|
||||||
```bash
|
```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 → Applications → Générer un token**
|
||||||
|
|
||||||
Pour régénérer un token Gitea : **Paramètres du compte Gitea → Applications → Générer un token**
|
|
||||||
Permissions requises : `repository` (lecture + écriture).
|
Permissions requises : `repository` (lecture + écriture).
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -385,7 +364,7 @@ https://votredomaine.fr/admin
|
|||||||
|
|
||||||
| Variable | Où la définir |
|
| 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
|
### 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
|
**Type** : Plateforme civique numérique
|
||||||
**Stack** : Python / Flask · React / Vite · PostgreSQL · Mistral AI
|
**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)
|
**Dépôt** : `voix-du-peuple` (Gitea)
|
||||||
**Statut** : Actif — avril 2026
|
**Statut** : Actif — avril 2026
|
||||||
**Version doc** : 1.4
|
**Version doc** : 1.4
|
||||||
|
|||||||
+2
-12
@@ -16,7 +16,7 @@
|
|||||||
# passed (e.g. an urgent security bugfix), you can add it to the
|
# passed (e.g. an urgent security bugfix), you can add it to the
|
||||||
# `minimumReleaseAgeExclude` allowlist below. Only consider doing this for
|
# `minimumReleaseAgeExclude` allowlist below. Only consider doing this for
|
||||||
# packages released by trusted organizations with an impeccable security
|
# 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.
|
# remove the exclusion once the 1-day window has passed.
|
||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
@@ -27,13 +27,6 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
minimumReleaseAge: 1440
|
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:
|
packages:
|
||||||
- artifacts/*
|
- artifacts/*
|
||||||
- lib/*
|
- lib/*
|
||||||
@@ -41,9 +34,6 @@ packages:
|
|||||||
- scripts
|
- scripts
|
||||||
|
|
||||||
catalog:
|
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
|
'@tailwindcss/vite': ^4.1.14
|
||||||
'@tanstack/react-query': ^5.90.21
|
'@tanstack/react-query': ^5.90.21
|
||||||
'@types/node': ^25.3.3
|
'@types/node': ^25.3.3
|
||||||
@@ -74,7 +64,7 @@ onlyBuiltDependencies:
|
|||||||
- unrs-resolver
|
- unrs-resolver
|
||||||
|
|
||||||
overrides:
|
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-arm64": "-"
|
||||||
"esbuild>@esbuild/darwin-x64": "-"
|
"esbuild>@esbuild/darwin-x64": "-"
|
||||||
"esbuild>@esbuild/freebsd-arm64": "-"
|
"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