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:
2026-05-23 22:41:06 +02:00
parent a7b7684e87
commit bc6bd3f9d7
28 changed files with 31 additions and 919 deletions
@@ -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"
-126
View File
@@ -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);
});
-33
View File
@@ -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"
}
}
-34
View File
@@ -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;
-25
View File
@@ -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");
});
-93
View File
@@ -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.";
}
}
-20
View File
@@ -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 },
},
}),
});
-11
View File
@@ -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;
-100
View File
@@ -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;
-12
View File
@@ -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;
-17
View File
@@ -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"
}
]
}
+7 -14
View File
@@ -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
-3
View File
@@ -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 -36
View File
@@ -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: {