Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ node_modules
coverage
test-results
playwright-report
graphify-out
*.pdf
blob-report
Dockerfile*
docker-compose*.yml
Expand Down
73 changes: 73 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:

permissions:
contents: write
packages: write

jobs:
release:
Expand Down Expand Up @@ -80,3 +81,75 @@ jobs:
la chaîne d'approvisionnement et corréler aux CVE publiées.

## Notes de release

Images Docker publiées sur GHCR pour cette release :

ghcr.io/association-dataring/louis:<version>
ghcr.io/association-dataring/louis-migrate:<version>

# Publication des images Docker sur GHCR : l'image app (standalone) et
# l'image migrate (drizzle-kit push one-shot) consommées par
# docker-compose.prod.yml et l'installeur scripts/install.sh.
docker:
name: Build and push Docker images (GHCR)
runs-on: ubuntu-latest
timeout-minutes: 90

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up QEMU (arm64)
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Image metadata (app)
id: meta_app
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=raw,value=latest

- name: Image metadata (migrate)
id: meta_migrate
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}-migrate
tags: |
type=semver,pattern={{version}}
type=raw,value=latest

- name: Build and push app image
uses: docker/build-push-action@v6
with:
context: .
target: runner
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta_app.outputs.tags }}
labels: ${{ steps.meta_app.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Build and push migrate image
uses: docker/build-push-action@v6
with:
context: .
target: migrator
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta_migrate.outputs.tags }}
labels: ${{ steps.meta_migrate.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
22 changes: 22 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,28 @@ ENV ENCRYPTION_KEY=build_placeholder_key_32_chars_long_for_aes256_scrypt_test

RUN npm run build

# ─────────────────────────────────────────────────────────────────────────────
# Migrator — image one-shot qui applique le schéma sur la base AVANT le
# démarrage de l'app (service `migrate` du docker-compose.prod.yml).
#
# Pourquoi une image séparée : le runner standalone ne contient ni drizzle-kit
# ni les sources du schéma (c'est ce qui le maintient à ~250 MB). Le schéma
# Louis s'applique par `drizzle-kit push` (déclaratif, idempotent) — `--force`
# car le conteneur n'a pas de TTY pour confirmer interactivement.
#
# Build : docker build --target migrator -t louis-migrate .
# Run : docker run --rm -e DATABASE_URL=… louis-migrate
# ─────────────────────────────────────────────────────────────────────────────
FROM node:24-alpine AS migrator
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY package.json tsconfig.json drizzle.config.ts ./
COPY src/db ./src/db
COPY scripts/setup-db.ts ./scripts/setup-db.ts

ENV NODE_ENV=production
CMD ["sh", "-c", "npx tsx scripts/setup-db.ts && npx drizzle-kit push --force"]

# ─────────────────────────────────────────────────────────────────────────────
FROM node:24-alpine AS runner
WORKDIR /app
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,21 @@ et [`docs/architecture/data-model.md`](./docs/architecture/data-model.md).
> Pour les autres modèles (Anthropic, OpenAI, Scaleway, OVH, Albert),
> les clés sont **optionnelles** et configurables une fois Louis lancé.

### Installation
### Installation en une commande (recommandée)

```bash
curl -fsSL https://raw.githubusercontent.com/Association-DataRing/Louis/main/scripts/install.sh | bash
```

Images pré-buildées (GHCR), secrets générés automatiquement, schéma appliqué
au démarrage — puis l'**assistant de premier lancement** (`/setup`) guide la
création du compte admin et la première clé IA dans le navigateur. Détails et
mise à jour : [docs/installation/one-command.md](./docs/installation/one-command.md).

> Seul prérequis : [Docker](https://docs.docker.com/get-docker/) (Compose v2).
> Node.js n'est nécessaire que pour l'installation depuis les sources ci-dessous.

### Installation depuis les sources (développement)

**1. Cloner et préparer les secrets**

Expand Down
124 changes: 124 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# ─────────────────────────────────────────────────────────────────────────────
# Louis — stack de production autonome
#
# Consommé par l'installeur une commande (scripts/install.sh), utilisable
# aussi directement :
#
# 1. créer un fichier .env à côté de ce fichier (secrets — voir ci-dessous)
# 2. docker compose -f docker-compose.prod.yml up -d
# 3. ouvrir http://localhost:3000 → l'assistant /setup prend le relais
#
# Variables .env attendues (générées automatiquement par install.sh) :
# POSTGRES_PASSWORD=… mot de passe Postgres interne
# AUTH_SECRET=… openssl rand -base64 32
# ENCRYPTION_KEY=… openssl rand -base64 32 (chiffrement des clés)
# S3_SECRET_ACCESS_KEY=… mot de passe MinIO interne
# Optionnelles :
# LOUIS_VERSION=v0.1.0 tag d'image (défaut : latest)
# LOUIS_IMAGE=ghcr.io/… registre alternatif (fork, miroir interne)
# LOUIS_PORT=3000 port d'écoute sur l'hôte
#
# Mise à jour :
# docker compose -f docker-compose.prod.yml pull
# docker compose -f docker-compose.prod.yml up -d
# (le service `migrate` ré-applique le schéma avant le redémarrage de l'app)
#
# Seul le port de l'app est exposé sur l'hôte — Postgres, Redis, MinIO et
# Gotenberg restent sur le réseau interne du compose. Mettre un reverse
# proxy TLS (Caddy/Nginx/Traefik) devant pour un accès distant.
# ─────────────────────────────────────────────────────────────────────────────

services:
postgres:
image: pgvector/pgvector:pg16
restart: unless-stopped
environment:
POSTGRES_USER: louis
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?définir POSTGRES_PASSWORD dans .env}
POSTGRES_DB: louis
volumes:
- louis-postgres:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U louis -d louis"]
interval: 5s
timeout: 5s
retries: 10

redis:
image: redis:7-alpine
restart: unless-stopped
volumes:
- louis-redis:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10

minio:
image: minio/minio:RELEASE.2025-09-07T16-13-09Z
restart: unless-stopped
command: server /data
environment:
MINIO_ROOT_USER: louis
MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:?définir S3_SECRET_ACCESS_KEY dans .env}
volumes:
- louis-minio:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 5s
timeout: 5s
retries: 10

# LibreOffice headless pour le rendu fidèle DOCX → PDF du DocPanel.
gotenberg:
image: gotenberg/gotenberg:8
restart: unless-stopped
command:
- "gotenberg"
- "--api-port=3000"
- "--api-timeout=60s"

# One-shot : applique le schéma (pgvector + drizzle-kit push) puis sort.
# L'app ne démarre qu'après sa réussite (service_completed_successfully).
migrate:
image: ${LOUIS_IMAGE:-ghcr.io/association-dataring/louis}-migrate:${LOUIS_VERSION:-latest}
restart: "no"
environment:
DATABASE_URL: postgresql://louis:${POSTGRES_PASSWORD}@postgres:5432/louis
depends_on:
postgres:
condition: service_healthy

app:
image: ${LOUIS_IMAGE:-ghcr.io/association-dataring/louis}:${LOUIS_VERSION:-latest}
restart: unless-stopped
ports:
- "${LOUIS_PORT:-3000}:3000"
environment:
NODE_ENV: production
DATABASE_URL: postgresql://louis:${POSTGRES_PASSWORD}@postgres:5432/louis
REDIS_URL: redis://redis:6379
S3_ENDPOINT: http://minio:9000
S3_REGION: eu-west-3
S3_BUCKET: louis
S3_ACCESS_KEY_ID: louis
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
# MinIO exige le path-style (la détection automatique ne couvre que
# les endpoints localhost, pas le hostname interne `minio`).
S3_FORCE_PATH_STYLE: "true"
GOTENBERG_URL: http://gotenberg:3000
AUTH_SECRET: ${AUTH_SECRET:?définir AUTH_SECRET dans .env}
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?définir ENCRYPTION_KEY dans .env}
depends_on:
migrate:
condition: service_completed_successfully
redis:
condition: service_healthy
minio:
condition: service_healthy

volumes:
louis-postgres:
louis-redis:
louis-minio:
70 changes: 70 additions & 0 deletions docs/installation/one-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Installation en une commande

La façon la plus simple d'installer Louis sur une machine (poste de cabinet,
serveur, VPS). Une seule dépendance : [Docker](https://docs.docker.com/get-docker/)
avec Compose v2.

```bash
curl -fsSL https://raw.githubusercontent.com/Association-DataRing/Louis/main/scripts/install.sh | bash
```

Le script :

1. vérifie que Docker tourne ;
2. crée un dossier `./louis` contenant le `docker-compose.prod.yml` et un
fichier `.env` avec des **secrets générés aléatoirement** (jamais écrasés
s'ils existent — relancer le script est sans danger) ;
3. télécharge les images publiées sur GHCR (app pré-buildée + migrateur de
schéma) et démarre les cinq services : Louis, PostgreSQL + pgvector,
Redis, MinIO, Gotenberg ;
4. applique le schéma de base automatiquement (service `migrate`, one-shot) ;
5. ouvre `http://localhost:3000` — **l'assistant de premier lancement**
prend le relais : compte administrateur, première clé IA (testée avant
enregistrement), et la première conversation est à un clic.

Aucun terminal n'est nécessaire après cette commande.

## Variables optionnelles

| Variable | Défaut | Rôle |
|---|---|---|
| `LOUIS_DIR` | `./louis` | dossier d'installation |
| `LOUIS_VERSION` | `latest` | tag d'image (ex. `v0.2.0`) |
| `LOUIS_PORT` | `3000` | port HTTP local |
| `LOUIS_REPO_RAW` | repo officiel | base raw GitHub (fork, miroir interne) |

```bash
LOUIS_PORT=8080 LOUIS_VERSION=v0.2.0 \
curl -fsSL https://raw.githubusercontent.com/Association-DataRing/Louis/main/scripts/install.sh | bash
```

## Mise à jour

```bash
cd louis
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```

Le service `migrate` ré-applique le schéma (idempotent) avant le redémarrage
de l'app — pas d'étape manuelle.

## Sauvegarde

Le fichier `louis/.env` contient `ENCRYPTION_KEY`, qui chiffre les clés API
stockées : **sa perte rend ces clés irrécupérables**. Sauvegardez-le avec vos
données (cf. [Sauvegarde et restauration](../admin/backups.md) pour la base).

## Accès distant

Seul le port de l'app est exposé. Pour servir Louis en HTTPS sur un domaine,
placez un reverse proxy TLS devant (Caddy, Nginx, Traefik) — voir les
exemples de la page [bare-metal](./bare-metal.md).

## Désinstallation

```bash
cd louis
docker compose -f docker-compose.prod.yml down # stop
docker compose -f docker-compose.prod.yml down -v # stop + données (irréversible)
```
25 changes: 25 additions & 0 deletions drizzle/migrations/0011_tool_observability.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
-- Télémétrie d'exécution des outils (connecteurs + MCP) : latence et
-- succès/échec PAR appel. Distinct de l'audit de conformité (audit_log) ; ici
-- on répond à « quel outil rame ou échoue, et à quelle fréquence ». Alimente
-- la section « Fiabilité des outils » de /settings/usage. Enregistrement
-- best-effort, scopé par utilisateur (null = contexte système).
-- Cf. lib/observability/tools.ts et lib/observability/query.ts.

CREATE TABLE IF NOT EXISTS "tool_invocations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid REFERENCES "users"("id") ON DELETE SET NULL,
"tool_name" text NOT NULL,
"category" text NOT NULL,
"success" boolean NOT NULL,
"error_reason" text,
"duration_ms" integer NOT NULL,
"created_at" timestamp NOT NULL DEFAULT now()
);

-- Agrégats « par outil sur la période » (page usage).
CREATE INDEX IF NOT EXISTS "tool_invocations_name_created_idx"
ON "tool_invocations" ("tool_name", "created_at");

-- Filtre « appels récents » + nettoyage par rétention.
CREATE INDEX IF NOT EXISTS "tool_invocations_created_idx"
ON "tool_invocations" ("created_at");
Loading
Loading