PolitiTrack - Architecture Technique
Date : 20/12/2025
Priorite : MOYENNE - FIL ROUGE
Status : Draft
Projet : Plateforme de transparence politique française
1. Vue d'ensemble
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND │
│ Qwik + TailwindCSS │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Fiches │ │ Votes │ │Contradic-│ │ Fact- │ │ Réseaux │ │
│ │ Élus │ │ Tracker │ │ tions │ │ Check │ │Influence │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ API GATEWAY │
│ Elysia (Bun) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ /api/ │ │ /api/ │ │ /api/ │ │ /api/ │ │
│ │politiques│ │ votes │ │ analyze │ │ search │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Supabase │ │ Redis │ │ Ollama │ │ Whisper │
│ PostgreSQL │ │ Cache │ │ LLM │ │ STT │
│ + pgvector │ │ Queue │ │ Analysis │ │ Transcribe │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ DATA COLLECTORS │
│ (Cron jobs / Workers) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │Assemblée │ │ Sénat │ │ HATVP │ │ Twitter │ │FactCheck │ │
│ │Nationale │ │ API │ │ Lobbies │ │ API │ │ Scraper │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────────────┘
2. Stack Technique
Frontend
| Techno |
Usage |
| Qwik |
Framework (resumability, SSR) |
| TailwindCSS |
Styling |
| D3.js / Vis.js |
Graphes relationnels |
| Chart.js |
Statistiques votes |
Backend
| Techno |
Usage |
| Elysia (Bun) |
API REST |
| Supabase |
Auth, DB, Storage |
| Redis |
Cache, Queue jobs |
| Puppeteer |
Scraping |
IA
| Techno |
Usage |
| Ollama (mistral:7b) |
Analyse contradictions, résumés |
| pgvector |
Embeddings pour recherche sémantique |
| Whisper |
Transcription vidéos/audios |
Infra
| Service |
Machine |
| Frontend + API |
prod-portainer |
| Supabase |
prod-portainer |
| Redis |
prod-portainer |
| Ollama |
win11 |
| Whisper |
prod-portainer |
3. Schéma Base de Données
-- Extensions requises
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "vector";
-- ============================================
-- TABLES PRINCIPALES
-- ============================================
-- Politiques (députés, sénateurs, ministres)
CREATE TABLE politiques (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL,
date_naissance DATE,
lieu_naissance VARCHAR(200),
photo_url TEXT,
wikipedia_url TEXT,
twitter_handle VARCHAR(50),
-- Parti actuel
parti_id UUID REFERENCES partis(id),
-- Métadonnées
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Historique des partis
CREATE TABLE historique_partis (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
parti_id UUID REFERENCES partis(id),
date_debut DATE NOT NULL,
date_fin DATE,
motif_depart TEXT
);
-- Partis politiques
CREATE TABLE partis (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
nom VARCHAR(200) NOT NULL,
sigle VARCHAR(20),
couleur VARCHAR(7), -- #RRGGBB
orientation VARCHAR(50), -- gauche, centre, droite, etc
logo_url TEXT
);
-- Mandats
CREATE TABLE mandats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
type VARCHAR(50) NOT NULL, -- depute, senateur, ministre, maire, etc
institution VARCHAR(100), -- Assemblée nationale, Sénat, Gouvernement
circonscription VARCHAR(200),
date_debut DATE NOT NULL,
date_fin DATE,
en_cours BOOLEAN DEFAULT true
);
-- ============================================
-- VOTES ET SCRUTINS
-- ============================================
-- Scrutins (les votes eux-mêmes)
CREATE TABLE scrutins (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
source_id VARCHAR(50) UNIQUE, -- ID de l'API source
titre TEXT NOT NULL,
description TEXT,
date_scrutin DATE NOT NULL,
type VARCHAR(50), -- loi, amendement, motion_censure, etc
-- Résultats
pour INTEGER,
contre INTEGER,
abstention INTEGER,
non_votants INTEGER,
adopte BOOLEAN,
-- Classification
themes TEXT[], -- économie, écologie, sécurité, etc
-- Source
source_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Votes individuels
CREATE TABLE votes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
scrutin_id UUID REFERENCES scrutins(id) ON DELETE CASCADE,
vote VARCHAR(20) NOT NULL, -- pour, contre, abstention, absent, non_votant
-- Comparaison groupe
groupe_majoritaire VARCHAR(20), -- vote majoritaire du groupe
contre_groupe BOOLEAN DEFAULT false,
UNIQUE(politique_id, scrutin_id)
);
-- Index pour recherche rapide
CREATE INDEX idx_votes_politique ON votes(politique_id);
CREATE INDEX idx_votes_scrutin ON votes(scrutin_id);
CREATE INDEX idx_votes_contre_groupe ON votes(contre_groupe) WHERE contre_groupe = true;
-- ============================================
-- DECLARATIONS ET CONTRADICTIONS
-- ============================================
-- Déclarations (tweets, interviews, discours)
CREATE TABLE declarations (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
contenu TEXT NOT NULL,
date_declaration TIMESTAMPTZ NOT NULL,
-- Source
source_type VARCHAR(50), -- tweet, interview_tv, interview_radio, article, discours
source_url TEXT,
source_media VARCHAR(200),
-- Transcription si audio/vidéo
transcription_whisper TEXT,
audio_url TEXT,
video_url TEXT,
-- Embedding pour recherche sémantique
embedding vector(384), -- dimension selon le modèle
-- Thèmes détectés par IA
themes TEXT[],
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Index vectoriel pour recherche sémantique
CREATE INDEX idx_declarations_embedding ON declarations
USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);
-- Contradictions détectées
CREATE TABLE contradictions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
-- Peut être entre 2 déclarations OU déclaration vs vote
declaration_1_id UUID REFERENCES declarations(id),
declaration_2_id UUID REFERENCES declarations(id),
vote_id UUID REFERENCES votes(id),
type VARCHAR(50), -- revirement, mensonge, incoherence, vote_vs_parole
-- Analyse IA
analyse_ia TEXT,
score_gravite INTEGER CHECK (score_gravite BETWEEN 1 AND 10),
-- Vérification humaine
verifie_humain BOOLEAN DEFAULT false,
verifie_par UUID, -- user_id si modération
verifie_at TIMESTAMPTZ,
-- Status
status VARCHAR(20) DEFAULT 'pending', -- pending, confirmed, rejected
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- FACT-CHECKING
-- ============================================
CREATE TABLE factchecks (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
-- La déclaration vérifiée
declaration_originale TEXT NOT NULL,
date_declaration DATE,
contexte TEXT,
-- Le verdict
verdict VARCHAR(30) NOT NULL, -- vrai, plutot_vrai, trompeur, exagere, faux
explication TEXT,
-- Source du fact-check
source VARCHAR(50), -- decodeurs, checknews, afp_factuel, etc
source_url TEXT NOT NULL,
date_factcheck DATE,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Score agrégé par politique
CREATE VIEW factcheck_scores AS
SELECT
politique_id,
COUNT(*) as total_checks,
COUNT(*) FILTER (WHERE verdict = 'vrai') as vrais,
COUNT(*) FILTER (WHERE verdict = 'plutot_vrai') as plutot_vrais,
COUNT(*) FILTER (WHERE verdict = 'trompeur') as trompeurs,
COUNT(*) FILTER (WHERE verdict = 'exagere') as exageres,
COUNT(*) FILTER (WHERE verdict = 'faux') as faux,
ROUND(
(COUNT(*) FILTER (WHERE verdict IN ('vrai', 'plutot_vrai'))::numeric /
NULLIF(COUNT(*), 0) * 100), 1
) as score_fiabilite
FROM factchecks
GROUP BY politique_id;
-- ============================================
-- LOBBIES ET INFLUENCES
-- ============================================
CREATE TABLE lobbies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
nom VARCHAR(300) NOT NULL,
sigle VARCHAR(50),
secteur VARCHAR(100), -- pharma, energie, tech, finance, etc
-- Données HATVP
hatvp_id VARCHAR(50) UNIQUE,
budget_declare DECIMAL(15,2),
effectif INTEGER,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE rencontres_lobbies (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
lobby_id UUID REFERENCES lobbies(id) ON DELETE CASCADE,
date_rencontre DATE,
contexte TEXT,
source_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Pantouflage (passage public/privé)
CREATE TABLE pantouflages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
politique_id UUID REFERENCES politiques(id) ON DELETE CASCADE,
poste_public VARCHAR(300),
date_fin_public DATE,
entreprise VARCHAR(300),
poste_prive VARCHAR(300),
date_debut_prive DATE,
delai_jours INTEGER, -- temps entre fin public et début privé
source_url TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- JOURNALISTES (optionnel)
-- ============================================
CREATE TABLE journalistes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
nom VARCHAR(100) NOT NULL,
prenom VARCHAR(100) NOT NULL,
photo_url TEXT,
twitter_handle VARCHAR(50),
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE journaliste_medias (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
journaliste_id UUID REFERENCES journalistes(id) ON DELETE CASCADE,
media_nom VARCHAR(200) NOT NULL,
media_proprietaire VARCHAR(200),
poste VARCHAR(100),
date_debut DATE,
date_fin DATE,
en_cours BOOLEAN DEFAULT true
);
-- ============================================
-- JOBS ET LOGS
-- ============================================
CREATE TABLE sync_jobs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
type VARCHAR(50), -- assemblee, senat, hatvp, factcheck, twitter
status VARCHAR(20) DEFAULT 'pending', -- pending, running, completed, failed
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
records_processed INTEGER DEFAULT 0,
error_message TEXT
);
4. API Endpoints
Politiques
GET /api/politiques # Liste avec pagination
GET /api/politiques/:id # Fiche détaillée
GET /api/politiques/:id/votes # Historique votes
GET /api/politiques/:id/declarations # Déclarations
GET /api/politiques/:id/contradictions # Contradictions
GET /api/politiques/:id/factchecks # Fact-checks
GET /api/politiques/:id/lobbies # Rencontres lobbies
GET /api/politiques/search?q= # Recherche
Votes
GET /api/votes # Tous les scrutins
GET /api/votes/:scrutin_id # Détail scrutin
GET /api/votes/:scrutin_id/resultats # Votes par politique
GET /api/votes/themes/:theme # Scrutins par thème
Contradictions
GET /api/contradictions # Dernières contradictions
GET /api/contradictions/:id # Détail
POST /api/contradictions/:id/verify # Vérification humaine
Fact-checks
GET /api/factchecks # Derniers fact-checks
GET /api/factchecks/ranking # Classement fiabilité
Recherche sémantique
POST /api/search/declarations # Recherche par sens
Body: { "query": "position sur le nucléaire" }
Analyse IA
POST /api/analyze/contradictions # Lancer analyse IA
POST /api/analyze/embeddings # Générer embeddings
5. Data Collectors (Workers)
Collector Assemblée Nationale
// /workers/assemblee-collector.ts
import { createClient } from '@supabase/supabase-js';
const ASSEMBLEE_API = 'https://data.assemblee-nationale.fr/api/v2';
async function syncDeputes() {
const response = await fetch(`${ASSEMBLEE_API}/deputes`);
const deputes = await response.json();
for (const depute of deputes) {
await supabase.from('politiques').upsert({
source_id: depute.id,
nom: depute.nom,
prenom: depute.prenom,
// ...
});
}
}
async function syncScrutins() {
const response = await fetch(`${ASSEMBLEE_API}/scrutins`);
const scrutins = await response.json();
for (const scrutin of scrutins) {
// Insérer scrutin
// Insérer votes individuels
}
}
// Cron: toutes les 6 heures
Collector Fact-checks
// /workers/factcheck-collector.ts
import puppeteer from 'puppeteer';
async function scrapeDecodeurs() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://www.lemonde.fr/les-decodeurs/');
const articles = await page.$$eval('.article', nodes =>
nodes.map(n => ({
titre: n.querySelector('h2')?.textContent,
url: n.querySelector('a')?.href,
date: n.querySelector('time')?.dateTime
}))
);
for (const article of articles) {
// Parser l'article
// Extraire politique mentionné
// Extraire verdict
// Insérer en base
}
await browser.close();
}
// Cron: toutes les 2 heures
Analyseur de Contradictions (IA)
// /workers/contradiction-analyzer.ts
async function analyzeContradictions(politiqueId: string) {
// 1. Récupérer toutes les déclarations
const declarations = await supabase
.from('declarations')
.select('*')
.eq('politique_id', politiqueId)
.order('date_declaration');
// 2. Récupérer les votes
const votes = await supabase
.from('votes')
.select('*, scrutins(*)')
.eq('politique_id', politiqueId);
// 3. Pour chaque paire de déclarations, demander à Ollama
for (let i = 0; i < declarations.length; i++) {
for (let j = i + 1; j < declarations.length; j++) {
const prompt = `
Analyse ces deux déclarations du même politique:
Déclaration 1 (${declarations[i].date}):
"${declarations[i].contenu}"
Déclaration 2 (${declarations[j].date}):
"${declarations[j].contenu}"
Y a-t-il une contradiction ou un revirement de position?
Réponds en JSON: {"contradiction": true/false, "type": "revirement|incoherence|aucune", "explication": "...", "gravite": 1-10}
`;
const response = await fetch('http://192.168.1.30:11434/api/generate', {
method: 'POST',
body: JSON.stringify({ model: 'mistral:7b', prompt })
});
// Parser et sauvegarder si contradiction
}
}
}
6. MVP - Phases de développement
Phase 1 : Fondations (2-3 semaines)
- [ ] Setup projet Qwik + Elysia
- [ ] Schéma Supabase
- [ ] Collector Assemblée Nationale (députés + votes)
- [ ] Page liste députés
- [ ] Page fiche député avec votes
Phase 2 : Enrichissement (2-3 semaines)
- [ ] Collector Sénat
- [ ] Collector HATVP (lobbies)
- [ ] Module fact-check (scraping Décodeurs)
- [ ] Score de fiabilité
Phase 3 : IA (2-3 semaines)
- [ ] Embeddings déclarations (pgvector)
- [ ] Recherche sémantique
- [ ] Détection contradictions (Ollama)
- [ ] Interface contradictions
Phase 4 : Visualisation (2 semaines)
- [ ] Graphe réseau d'influence (D3.js)
- [ ] Timeline positions
- [ ] Dashboard statistiques
Phase 5 : Polish (1-2 semaines)
- [ ] Responsive mobile
- [ ] SEO
- [ ] Performance
- [ ] Déploiement public
7. Déploiement
Containers Docker
# docker-compose.yml
version: '3.8'
services:
polititrack-frontend:
build: ./frontend
ports:
- "4600:3000"
environment:
- API_URL=http://polititrack-api:3001
polititrack-api:
build: ./api
ports:
- "4601:3001"
environment:
- SUPABASE_URL=${SUPABASE_PROD_URL}
- SUPABASE_KEY=${SUPABASE_PROD_SERVICE_ROLE_KEY}
- REDIS_URL=redis://prod-redis.local:6379
- OLLAMA_URL=http://192.168.1.30:11434
polititrack-workers:
build: ./workers
environment:
- SUPABASE_URL=${SUPABASE_PROD_URL}
- SUPABASE_KEY=${SUPABASE_PROD_SERVICE_ROLE_KEY}
URLs
8. Ressources utiles
APIs officielles
Projets similaires (inspiration)
Documentation