33800 Docs

← Retour

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)

Phase 2 : Enrichissement (2-3 semaines)

Phase 3 : IA (2-3 semaines)

Phase 4 : Visualisation (2 semaines)

Phase 5 : Polish (1-2 semaines)


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

Service URL
Frontend https://polititrack.33800.nowhere84.com
API https://polititrack-api.33800.nowhere84.com

8. Ressources utiles

APIs officielles

Projets similaires (inspiration)

Documentation