Compare commits

..

No commits in common. "0ce1752aed8cbdd20d70fb4d11b5fd7eb87849cd" and "7c5d9d12256149de98cda06a3aab06515f1a6d99" have entirely different histories.

21 changed files with 3736 additions and 120 deletions

View File

@ -1,48 +0,0 @@
# ─────────────────────────────────────────────────────────────────────────────
# Veylant IA — Environment variables
# Copy this file to .env and fill in the values.
# All VEYLANT_* vars override the corresponding key in config.yaml.
# ─────────────────────────────────────────────────────────────────────────────
# ── Server ────────────────────────────────────────────────────────────────────
VEYLANT_SERVER_ENV=development
VEYLANT_SERVER_TENANT_NAME=My Organisation
VEYLANT_SERVER_ALLOWED_ORIGINS=http://localhost:3000
# ── Auth (JWT) ────────────────────────────────────────────────────────────────
# Generate: openssl rand -hex 32
VEYLANT_AUTH_JWT_SECRET=change-me-in-production
VEYLANT_AUTH_JWT_TTL_HOURS=24
# ── Database ──────────────────────────────────────────────────────────────────
VEYLANT_DATABASE_URL=postgres://veylant:veylant_dev@localhost:5432/veylant?sslmode=disable
# ── Redis ─────────────────────────────────────────────────────────────────────
VEYLANT_REDIS_URL=redis://localhost:6379
# ── ClickHouse ────────────────────────────────────────────────────────────────
VEYLANT_CLICKHOUSE_DSN=clickhouse://veylant:veylant_dev@localhost:9000/veylant_logs
# ── Cryptography ──────────────────────────────────────────────────────────────
# AES-256-GCM key for prompt encryption. Generate: openssl rand -base64 32
VEYLANT_CRYPTO_AES_KEY_BASE64=
# ── LLM Provider API Keys ─────────────────────────────────────────────────────
VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-...
VEYLANT_PROVIDERS_ANTHROPIC_API_KEY=sk-ant-...
VEYLANT_PROVIDERS_MISTRAL_API_KEY=
VEYLANT_PROVIDERS_AZURE_API_KEY=
VEYLANT_PROVIDERS_AZURE_RESOURCE_NAME=
VEYLANT_PROVIDERS_AZURE_DEPLOYMENT_ID=
# ── SMTP (email notifications) ────────────────────────────────────────────────
VEYLANT_NOTIFICATIONS_SMTP_HOST=smtp.example.com
VEYLANT_NOTIFICATIONS_SMTP_PORT=587
VEYLANT_NOTIFICATIONS_SMTP_USERNAME=alerts@example.com
VEYLANT_NOTIFICATIONS_SMTP_PASSWORD=your-smtp-password
VEYLANT_NOTIFICATIONS_SMTP_FROM=noreply@example.com
VEYLANT_NOTIFICATIONS_SMTP_FROM_NAME=Veylant IA
# ── HashiCorp Vault (production only) ─────────────────────────────────────────
# VAULT_ADDR=https://vault.example.com
# VAULT_TOKEN=

61
.gitignore vendored
View File

@ -1,4 +1,4 @@
# ─── Go ─────────────────────────────────────────────────────────────────────── # Go
bin/ bin/
*.exe *.exe
*.exe~ *.exe~
@ -9,13 +9,15 @@ bin/
*.out *.out
coverage.out coverage.out
coverage.html coverage.html
coverage_internal.out
coverage/ # Vendor
go.work
go.work.sum
vendor/ vendor/
# ─── Python ─────────────────────────────────────────────────────────────────── # Go workspace
go.work
go.work.sum
# Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
@ -23,27 +25,20 @@ __pycache__/
.venv/ .venv/
venv/ venv/
env/ env/
dist/
*.egg-info/ *.egg-info/
.pytest_cache/ .pytest_cache/
.mypy_cache/ .mypy_cache/
htmlcov/ htmlcov/
.ruff_cache/
# ─── Node / Frontend ────────────────────────────────────────────────────────── # Node / Frontend
node_modules/ node_modules/
.next/ .next/
out/ out/
dist/ dist/
*.local *.local
web/dist/
web/.vite/
# ─── web-public (standalone public site — has its own build/deploy) ─────────── # Environment & secrets
web-public/
# ─── Secrets & config ─────────────────────────────────────────────────────────
# Real config lives in config.yaml — use config.yaml.example as the template
config.yaml
.env .env
.env.* .env.*
!.env.example !.env.example
@ -54,37 +49,31 @@ config.yaml
secrets/ secrets/
vault-tokens/ vault-tokens/
# ─── Generated proto stubs (regenerated via `make proto`) ───────────────────── # Docker
gen/ .docker/
services/pii/gen/
# ─── Terraform state ────────────────────────────────────────────────────────── # Terraform
.terraform/ .terraform/
*.tfstate *.tfstate
*.tfstate.* *.tfstate.*
*.tfplan *.tfplan
.terraform.lock.hcl .terraform.lock.hcl
# ─── Docker ─────────────────────────────────────────────────────────────────── # IDE
.docker/
# ─── Logs & temp ──────────────────────────────────────────────────────────────
*.log
logs/
tmp/
*.tmp
# ─── Test / scratch files ─────────────────────────────────────────────────────
test_smtp.go
# ─── IDE ──────────────────────────────────────────────────────────────────────
.idea/ .idea/
.vscode/ .vscode/
*.swp *.swp
*.swo *.swo
*~ *~
.DS_Store .DS_Store
Thumbs.db
# ─── Compiled proxy binary ──────────────────────────────────────────────────── # Generated proto stubs
proxy gen/
services/pii/gen/
# Logs
*.log
logs/
# Coverage reports
coverage/

View File

@ -1,10 +1,10 @@
server: server:
port: 8090 port: 8090
shutdown_timeout_seconds: 30 shutdown_timeout_seconds: 30
env: development # "production" → fatal on any missing service env: development
tenant_name: "My Organisation" tenant_name: "Mon Organisation"
# CORS: origins allowed to call the proxy from a browser. # CORS: origins allowed to call the proxy from a browser (React dashboard).
# Override in production: VEYLANT_SERVER_ALLOWED_ORIGINS=https://dashboard.example.com # Override in production: VEYLANT_SERVER_ALLOWED_ORIGINS=https://dashboard.veylant.ai
allowed_origins: allowed_origins:
- "http://localhost:3000" - "http://localhost:3000"
@ -17,32 +17,28 @@ database:
redis: redis:
url: "redis://localhost:6379" url: "redis://localhost:6379"
# Local JWT authentication (email/password). # Local JWT authentication (email/password — replaces Keycloak).
# MUST be changed in production — use a long random secret. # Override jwt_secret in production via VEYLANT_AUTH_JWT_SECRET.
# Generate: openssl rand -hex 32
# Override: VEYLANT_AUTH_JWT_SECRET=<your-secret>
auth: auth:
jwt_secret: "change-me-in-production" jwt_secret: "change-me-in-production-use-VEYLANT_AUTH_JWT_SECRET"
jwt_ttl_hours: 24 jwt_ttl_hours: 24
pii: pii:
enabled: true enabled: true
service_addr: "localhost:50051" service_addr: "localhost:50051"
timeout_ms: 100 timeout_ms: 100
fail_open: true # set false in production fail_open: true
log: log:
level: "info" # debug | info | warn | error level: "info"
format: "json" # json | console format: "json"
# LLM provider adapters. # LLM provider adapters.
# API keys MUST be injected via env vars — never hardcode them here. # Sensitive values (API keys) must be injected via env vars — never hardcode them.
# Example: VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-... # Example: VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-...
# Provider configs can also be managed via the admin API (POST /v1/admin/providers).
providers: providers:
openai: openai:
base_url: "https://api.openai.com/v1" base_url: "https://api.openai.com/v1"
# api_key: set via VEYLANT_PROVIDERS_OPENAI_API_KEY
timeout_seconds: 30 timeout_seconds: 30
max_conns: 100 max_conns: 100
@ -58,8 +54,8 @@ providers:
timeout_seconds: 30 timeout_seconds: 30
max_conns: 100 max_conns: 100
# api_key: set via VEYLANT_PROVIDERS_AZURE_API_KEY # api_key: set via VEYLANT_PROVIDERS_AZURE_API_KEY
# resource_name: set via VEYLANT_PROVIDERS_AZURE_RESOURCE_NAME # resource_name: set via VEYLANT_PROVIDERS_AZURE_RESOURCE_NAME (e.g. "my-azure-resource")
# deployment_id: set via VEYLANT_PROVIDERS_AZURE_DEPLOYMENT_ID # deployment_id: set via VEYLANT_PROVIDERS_AZURE_DEPLOYMENT_ID (e.g. "gpt-4o")
mistral: mistral:
base_url: "https://api.mistral.ai/v1" base_url: "https://api.mistral.ai/v1"
@ -73,9 +69,10 @@ providers:
max_conns: 10 max_conns: 10
# Role-based access control for the provider router. # Role-based access control for the provider router.
# Controls which models each role can access.
rbac: rbac:
# Models accessible to the "user" role (exact match or prefix). # Models accessible to the "user" role (exact match or prefix, e.g. "gpt-4o-mini" matches "gpt-4o-mini-2024-07-18").
# admin and manager always have unrestricted access. # admin and manager roles always have unrestricted access.
user_allowed_models: user_allowed_models:
- "gpt-4o-mini" - "gpt-4o-mini"
- "gpt-3.5-turbo" - "gpt-3.5-turbo"
@ -88,44 +85,45 @@ metrics:
path: "/metrics" path: "/metrics"
# Intelligent routing engine. # Intelligent routing engine.
# Rules are stored in the routing_rules table and cached per tenant.
routing: routing:
# How long routing rules are cached in memory before a background refresh. # How long routing rules are cached in memory before a background refresh.
# Admin mutations call Invalidate() immediately regardless of this TTL.
cache_ttl_seconds: 30 cache_ttl_seconds: 30
# ClickHouse audit log. # ClickHouse audit log (Sprint 6).
# DSN: clickhouse://user:pass@host:9000/database # DSN: clickhouse://user:pass@host:9000/database
# Override: VEYLANT_CLICKHOUSE_DSN=clickhouse://... # Set via env var: VEYLANT_CLICKHOUSE_DSN
clickhouse: clickhouse:
dsn: "clickhouse://veylant:veylant_dev@localhost:9000/veylant_logs" dsn: "clickhouse://veylant:veylant_dev@localhost:9000/veylant_logs"
max_conns: 10 max_conns: 10
dial_timeout_seconds: 5 dial_timeout_seconds: 5
# Cryptography. # Cryptography settings.
# AES-256-GCM key for encrypting stored prompts. # AES-256-GCM key for encrypting prompt_anonymized in the audit log.
# MUST be set in production via: VEYLANT_CRYPTO_AES_KEY_BASE64 # MUST be set via env var in production: VEYLANT_CRYPTO_AES_KEY_BASE64
# Generate: openssl rand -base64 32 # Generate: openssl rand -base64 32
crypto: crypto:
# Development placeholder — override in production via env var.
aes_key_base64: "" aes_key_base64: ""
# Rate limiting defaults. Per-tenant overrides stored in the rate_limit_configs table. # Rate limiting defaults. Per-tenant overrides are stored in rate_limit_configs table.
# Override via env: VEYLANT_RATE_LIMIT_DEFAULT_TENANT_RPM, VEYLANT_RATE_LIMIT_DEFAULT_USER_RPM, etc.
rate_limit: rate_limit:
default_tenant_rpm: 1000 default_tenant_rpm: 1000
default_tenant_burst: 200 default_tenant_burst: 200
default_user_rpm: 100 default_user_rpm: 100
default_user_burst: 20 default_user_burst: 20
# Email notifications via SMTP. # Email notifications via SMTP (Mailtrap sandbox).
# Override credentials in production via env vars: # Override password in production: VEYLANT_NOTIFICATIONS_SMTP_PASSWORD=<full_password>
# VEYLANT_NOTIFICATIONS_SMTP_HOST # Full password visible in Mailtrap dashboard → Sending → SMTP Settings → Show credentials.
# VEYLANT_NOTIFICATIONS_SMTP_PORT
# VEYLANT_NOTIFICATIONS_SMTP_USERNAME
# VEYLANT_NOTIFICATIONS_SMTP_PASSWORD
# VEYLANT_NOTIFICATIONS_SMTP_FROM
notifications: notifications:
smtp: smtp:
host: "smtp.example.com" host: "sandbox.smtp.mailtrap.io"
port: 587 port: 587
username: "alerts@example.com" username: "2597bd31d265eb"
password: "your-smtp-password" # Mailtrap password — replace ****3c89 with the full value from the dashboard.
from: "noreply@example.com" password: "cd126234193c89"
from: "noreply@veylant.ai"
from_name: "Veylant IA" from_name: "Veylant IA"

BIN
proxy Executable file

Binary file not shown.

67
test_smtp.go Normal file
View File

@ -0,0 +1,67 @@
//go:build ignore
// Quick SMTP diagnostic — run with:
// SMTP_USER=dharnaud77@gmail.com SMTP_PASS=xsmtpsib-... go run test_smtp.go
package main
import (
"crypto/tls"
"fmt"
"net"
"net/smtp"
"os"
)
func main() {
host := "smtp-relay.brevo.com"
port := 587
user := os.Getenv("SMTP_USER")
pass := os.Getenv("SMTP_PASS")
if user == "" || pass == "" {
fmt.Fprintln(os.Stderr, "Usage: SMTP_USER=... SMTP_PASS=... go run test_smtp.go")
os.Exit(1)
}
addr := fmt.Sprintf("%s:%d", host, port)
fmt.Printf("Dialing %s ...\n", addr)
conn, err := net.Dial("tcp", addr)
if err != nil {
fmt.Printf("FAIL dial: %v\n", err)
os.Exit(1)
}
client, err := smtp.NewClient(conn, host)
if err != nil {
fmt.Printf("FAIL smtp.NewClient: %v\n", err)
os.Exit(1)
}
defer client.Close() //nolint:errcheck
if ok, _ := client.Extension("STARTTLS"); ok {
fmt.Println("OK STARTTLS advertised")
if err = client.StartTLS(&tls.Config{ServerName: host}); err != nil {
fmt.Printf("FAIL StartTLS: %v\n", err)
os.Exit(1)
}
fmt.Println("OK STARTTLS negotiated")
} else {
fmt.Println("WARN STARTTLS not advertised — credentials will be sent in clear")
}
if ok, params := client.Extension("AUTH"); ok {
fmt.Printf("OK AUTH methods advertised: %s\n", params)
} else {
fmt.Println("WARN no AUTH advertised in EHLO")
}
auth := smtp.PlainAuth("", user, pass, host)
if err = client.Auth(auth); err != nil {
fmt.Printf("FAIL AUTH PLAIN: %v\n\n", err)
fmt.Println("→ Le SMTP key est invalide ou révoqué.")
fmt.Println(" Génère-en un nouveau sur app.brevo.com → Settings → SMTP & API → SMTP Keys")
os.Exit(1)
}
fmt.Println("OK AUTH PLAIN success — credentials valides !")
_ = client.Quit()
}

39
web-public/Dockerfile Normal file
View File

@ -0,0 +1,39 @@
# ─── Stage 1: Build ──────────────────────────────────────────────────────────
# Build context must be the PROJECT ROOT (not web-public/) so that the shared
# doc pages in web/src/pages/docs/ are accessible during the build.
#
# docker build -f web-public/Dockerfile -t veylant-public .
#
FROM node:20-alpine AS build
WORKDIR /build
# Install dependencies for the public site
COPY web-public/package.json web-public/package-lock.json* ./web-public/
RUN cd web-public && npm ci --prefer-offline
# Copy the source of the public site
COPY web-public/ ./web-public/
# Copy the shared documentation pages from the main web app
# (vite.config resolves @docs → ../web/src/pages/docs)
COPY web/src/pages/docs/ ./web/src/pages/docs/
# Build the production bundle
ARG VITE_DASHBOARD_URL=http://localhost:3000
ARG VITE_PLAYGROUND_URL=http://localhost:8090/playground
ENV VITE_DASHBOARD_URL=$VITE_DASHBOARD_URL
ENV VITE_PLAYGROUND_URL=$VITE_PLAYGROUND_URL
RUN cd web-public && npm run build
# ─── Stage 2: Serve ──────────────────────────────────────────────────────────
FROM nginx:1.27-alpine
COPY --from=build /build/web-public/dist /usr/share/nginx/html
COPY web-public/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost/index.html || exit 1

View File

@ -0,0 +1,53 @@
# ─────────────────────────────────────────────────────────────────────────────
# Veylant IA — Public Site Stack (Landing + Documentation)
# Deploy this file in Portainer as a standalone stack.
#
# Variables to set in Portainer → Stacks → Environment:
# VITE_DASHBOARD_URL URL of the Veylant dashboard app (default: http://localhost:3000)
# VITE_PLAYGROUND_URL URL of the proxy playground (default: http://localhost:8090/playground)
# IMAGE_TAG Docker image tag to deploy (default: latest)
# ─────────────────────────────────────────────────────────────────────────────
version: "3.8"
services:
veylant-public:
image: ghcr.io/veylant/ia-public:${IMAGE_TAG:-latest}
# ── Build locally (comment out "image:" above and uncomment this block) ──
# build:
# context: .. # project root
# dockerfile: web-public/Dockerfile
# args:
# VITE_DASHBOARD_URL: ${VITE_DASHBOARD_URL:-http://localhost:3000}
# VITE_PLAYGROUND_URL: ${VITE_PLAYGROUND_URL:-http://localhost:8090/playground}
ports:
- "3001:80"
environment:
- VITE_DASHBOARD_URL=${VITE_DASHBOARD_URL:-http://localhost:3000}
- VITE_PLAYGROUND_URL=${VITE_PLAYGROUND_URL:-http://localhost:8090/playground}
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/index.html"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
labels:
# Traefik labels — uncomment if using Traefik as reverse proxy
# - "traefik.enable=true"
# - "traefik.http.routers.veylant-public.rule=Host(`veylant.io`)"
# - "traefik.http.routers.veylant-public.entrypoints=websecure"
# - "traefik.http.routers.veylant-public.tls.certresolver=letsencrypt"
# - "traefik.http.services.veylant-public.loadbalancer.server.port=80"
com.veylant.service: "public-site"
com.veylant.version: "${IMAGE_TAG:-latest}"
networks:
default:
name: veylant-public-network

14
web-public/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Veylant IA — Proxy IA d'entreprise. Anonymisation PII, gouvernance RGPD, contrôle des coûts LLM." />
<title>Veylant IA — Proxy IA d'entreprise</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

29
web-public/nginx.conf Normal file
View File

@ -0,0 +1,29 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https:;" always;
# Gzip
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript;
# Static assets long cache
location ~* \.(js|css|png|jpg|jpeg|svg|ico|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# SPA fallback all routes served by index.html
location / {
try_files $uri $uri/ /index.html;
}
}

2768
web-public/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

29
web-public/package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "veylant-public",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview --port 3001",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"@types/node": "^22.5.5",
"@types/react": "^18.3.5",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.45",
"tailwindcss": "^3.4.11",
"typescript": "^5.5.3",
"vite": "^5.4.3"
}
}

View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -0,0 +1,24 @@
// Minimal cn() utility — mirrors web/src/lib/utils.ts
// The doc components import @/lib/utils; this file satisfies that import
// without pulling in the full dashboard dependency tree.
type ClassValue = string | number | boolean | undefined | null | ClassValue[];
function clsx(...args: ClassValue[]): string {
const classes: string[] = [];
for (const arg of args) {
if (!arg) continue;
if (typeof arg === "string" || typeof arg === "number") {
classes.push(String(arg));
} else if (Array.isArray(arg)) {
const inner = clsx(...arg);
if (inner) classes.push(inner);
}
}
return classes.join(" ");
}
// Simplified tw-merge: just join classes (no conflict resolution needed for docs)
export function cn(...inputs: ClassValue[]): string {
return clsx(...inputs);
}

11
web-public/src/main.tsx Normal file
View File

@ -0,0 +1,11 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { router } from "./router";
import "./styles.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
);

View File

@ -0,0 +1,382 @@
import { Link } from "react-router-dom";
const GITHUB_URL = "https://github.com/DH7789-dev/Veylant-IA";
const PLAYGROUND_URL = import.meta.env.VITE_PLAYGROUND_URL ?? "http://localhost:8090/playground";
const brand = "#4f46e5";
const brandLight = "#818cf8";
const accent = "#06b6d4";
const bg = "#0a0f1e";
const card = "rgba(255,255,255,0.04)";
const border = "rgba(255,255,255,0.08)";
const muted = "#94a3b8";
const dim = "#64748b";
const features = [
{
icon: "🔍",
title: "Anonymisation PII automatique",
body: "3 couches de détection (regex, Presidio NER, validation LLM). Pseudonymisation réversible avec mapping chiffré AES-256-GCM en Redis.",
badge: "latence <50ms",
},
{
icon: "🧭",
title: "Routage intelligent",
body: "Moteur de règles basé sur le rôle, le département, la sensibilité et le modèle demandé. Fallback automatique en cas de panne provider.",
badge: "circuit breaker intégré",
},
{
icon: "🛡️",
title: "RBAC granulaire",
body: "4 rôles (admin, manager, user, auditor) avec contrôle par modèle et par département. JWT HS256 natif, SSO en V2.",
badge: "JWT natif",
},
{
icon: "📋",
title: "Logs d'audit immuables",
body: "Chaque requête est enregistrée dans ClickHouse (append-only). Impossible à modifier, rétention configurable, export CSV/PDF pour la CNIL.",
badge: "RGPD Art. 30",
},
{
icon: "⚖️",
title: "Conformité RGPD & EU AI Act",
body: "Registre de traitement automatique, classification des risques AI Act (5 questions), rapports PDF téléchargeables. DPO-ready dès le premier jour.",
badge: "EU AI Act ready",
},
{
icon: "📊",
title: "Contrôle des coûts",
body: "Suivi des tokens par tenant, utilisateur et département. Alertes budgétaires par email, tableaux de bord Grafana, imputation par centre de coût.",
badge: "dashboard temps réel",
},
];
const problems = [
{
icon: "🕵️",
title: "Shadow AI",
body: "73 % des entreprises ont des employés utilisant des outils IA non approuvés. Données clients, contrats, code propriétaire partent vers des serveurs tiers sans votre accord.",
},
{
icon: "🔓",
title: "Fuites de données PII",
body: "Sans filtre, vos prompts contiennent noms, IBAN, emails, numéros de sécurité sociale. Une seule fuite = violation RGPD notifiable à la CNIL sous 72h.",
},
{
icon: "💸",
title: "Coûts hors de contrôle",
body: "Les abonnements prolifèrent, les tokens s'accumulent. Sans visibilité centralisée, la facture IA gonfle sans corrélation avec la valeur produite.",
},
];
const personas = [
{
role: "RSSI",
title: "Responsable Sécurité SI",
items: [
"Visibilité complète sur tous les flux LLM",
"Zero Trust, mTLS, AES-256-GCM",
"Rapport pentest disponible sur demande",
"Alertes temps réel (PagerDuty, Slack)",
"Shadow AI éliminé structurellement",
],
},
{
role: "DSI",
title: "Directeur Systèmes d'Information",
items: [
"Déploiement Helm/Kubernetes en 15 min",
"Compatible tout SDK OpenAI existant",
"Multi-provider avec fallback automatique",
"Dashboard coûts par équipe et projet",
"HPA autoscaling, blue/green deployment",
],
},
{
role: "DPO",
title: "Data Protection Officer",
items: [
"Registre Art. 30 généré automatiquement",
"Classification risques EU AI Act intégrée",
"Export PDF pour audits CNIL",
"Pseudonymisation réversible et traçable",
"Rétention configurable par type de donnée",
],
},
];
function Label({ children }: { children: React.ReactNode }) {
return (
<div style={{ display: "inline-flex", alignItems: "center", gap: "0.4rem", fontSize: "0.72rem", fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.1em", color: brandLight, background: "rgba(79,70,229,0.15)", border: `1px solid rgba(79,70,229,0.3)`, padding: "0.3rem 0.85rem", borderRadius: "100px", marginBottom: "1.5rem" }}>
{children}
</div>
);
}
function GradientText({ children }: { children: React.ReactNode }) {
return (
<span style={{ background: `linear-gradient(135deg, #a5b4fc 0%, ${accent} 100%)`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text" }}>
{children}
</span>
);
}
function BtnPrimary({ href, to, children }: { href?: string; to?: string; children: React.ReactNode }) {
const style: React.CSSProperties = { display: "inline-flex", alignItems: "center", gap: "0.45rem", padding: "0.85rem 2rem", borderRadius: "10px", fontSize: "1rem", fontWeight: 600, textDecoration: "none", cursor: "pointer", border: "none", background: brand, color: "#fff", transition: "all .2s", whiteSpace: "nowrap" };
if (to) return <Link to={to} style={style}>{children}</Link>;
return <a href={href} style={style}>{children}</a>;
}
function BtnOutline({ href, to, target, rel, children }: { href?: string; to?: string; target?: string; rel?: string; children: React.ReactNode }) {
const style: React.CSSProperties = { display: "inline-flex", alignItems: "center", gap: "0.45rem", padding: "0.85rem 2rem", borderRadius: "10px", fontSize: "1rem", fontWeight: 600, textDecoration: "none", background: "transparent", color: "#f1f5f9", border: `1px solid ${border}`, transition: "all .2s", whiteSpace: "nowrap" };
if (to) return <Link to={to} style={style}>{children}</Link>;
return <a href={href} target={target} rel={rel} style={style}>{children}</a>;
}
export function LandingPage() {
return (
<div style={{ fontFamily: "system-ui, -apple-system, 'Segoe UI', sans-serif", background: bg, color: "#f1f5f9", minHeight: "100vh", overflowX: "hidden" }}>
{/* Background glow */}
<div style={{ position: "fixed", inset: 0, background: "radial-gradient(ellipse at 15% 50%, rgba(79,70,229,.14) 0%, transparent 55%), radial-gradient(ellipse at 85% 15%, rgba(6,182,212,.09) 0%, transparent 50%)", pointerEvents: "none", zIndex: 0 }} />
{/* NAV */}
<nav style={{ position: "sticky", top: 0, zIndex: 100, backdropFilter: "blur(14px)", WebkitBackdropFilter: "blur(14px)", background: "rgba(10,15,30,.88)", borderBottom: `1px solid ${border}` }}>
<div style={{ maxWidth: 1200, margin: "0 auto", padding: "0 1.5rem", height: 64, display: "flex", alignItems: "center", gap: "2rem" }}>
<Link to="/" style={{ fontSize: "1.25rem", fontWeight: 800, color: "#f1f5f9", textDecoration: "none", letterSpacing: "-0.5px", flexShrink: 0 }}>
Veylant<span style={{ color: brandLight }}> IA</span>
</Link>
<div style={{ display: "flex", gap: "1.75rem", marginLeft: "auto", alignItems: "center" }}>
<a href="#fonctionnalites" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Fonctionnalités</a>
<a href="#securite" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Sécurité</a>
<a href="#personas" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Pour qui</a>
<a href={PLAYGROUND_URL} style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Playground</a>
<Link to="/docs" style={{ color: muted, textDecoration: "none", fontSize: ".875rem", fontWeight: 500 }}>Docs</Link>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" style={{ display: "inline-flex", alignItems: "center", gap: ".4rem", background: "rgba(255,255,255,0.07)", color: "#f1f5f9", padding: ".5rem 1.1rem", borderRadius: 8, fontSize: ".875rem", fontWeight: 600, textDecoration: "none", border: `1px solid ${border}` }}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z"/></svg>
GitHub
</a>
</div>
</div>
</nav>
{/* HERO */}
<section style={{ padding: "7rem 1.5rem 5rem", position: "relative", zIndex: 1 }}>
<div style={{ maxWidth: 1200, margin: "0 auto", display: "grid", gridTemplateColumns: "1fr 1fr", gap: "4rem", alignItems: "center" }}>
<div>
<Label>
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
Proxy IA d'entreprise
</Label>
<h1 style={{ fontSize: "clamp(2.4rem,5vw,3.8rem)", fontWeight: 800, lineHeight: 1.15, letterSpacing: "-0.025em", marginBottom: "1.25rem" }}>
L'IA de vos équipes,<br />
<GradientText>enfin sous contrôle</GradientText>
</h1>
<p style={{ fontSize: "1.1rem", color: muted, lineHeight: 1.75, marginBottom: "2rem", maxWidth: 500 }}>
Veylant intercepte, anonymise et gouverne tous vos échanges LLM en moins de 50 ms. RGPD natif, EU AI Act prêt, zéro Shadow AI.
</p>
<div style={{ display: "flex", gap: ".875rem", flexWrap: "wrap" }}>
<BtnPrimary href="#contact">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><polyline points="10 17 15 12 10 7"/><line x1="15" y1="12" x2="3" y2="12"/></svg>
Demander une démo
</BtnPrimary>
<BtnOutline href={PLAYGROUND_URL}>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><polygon points="5 3 19 12 5 21 5 3"/></svg>
Tester le playground
</BtnOutline>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: "1rem", marginTop: "3.5rem", paddingTop: "2.5rem", borderTop: `1px solid ${border}` }}>
{[
{ val: "<50ms", lbl: "Latence pipeline PII" },
{ val: "5", lbl: "Providers LLM" },
{ val: "0", lbl: "Shadow AI autorisé" },
{ val: "100%", lbl: "Compatible OpenAI SDK" },
].map((s) => (
<div key={s.lbl} style={{ textAlign: "center" }}>
<div style={{ fontSize: "1.7rem", fontWeight: 800, letterSpacing: "-0.04em", background: `linear-gradient(135deg, #a5b4fc, ${accent})`, WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent", backgroundClip: "text" }}>{s.val}</div>
<div style={{ fontSize: ".78rem", color: muted, marginTop: ".2rem" }}>{s.lbl}</div>
</div>
))}
</div>
</div>
{/* Terminal */}
<div style={{ background: "#0d1117", border: `1px solid rgba(255,255,255,.1)`, borderRadius: 20, overflow: "hidden", boxShadow: "0 30px 60px rgba(0,0,0,.6)" }}>
<div style={{ background: "#161b22", padding: ".7rem 1rem", display: "flex", alignItems: "center", gap: ".4rem", borderBottom: `1px solid rgba(255,255,255,.07)` }}>
<div style={{ width: 12, height: 12, borderRadius: "50%", background: "#ff5f57" }} />
<div style={{ width: 12, height: 12, borderRadius: "50%", background: "#ffbd2e" }} />
<div style={{ width: 12, height: 12, borderRadius: "50%", background: "#28c840" }} />
<span style={{ marginLeft: ".5rem", fontSize: ".72rem", color: dim, fontFamily: "monospace" }}>veylant-proxy requête interceptée</span>
</div>
<div style={{ padding: "1.5rem", fontFamily: "monospace", fontSize: ".76rem", lineHeight: 1.9 }}>
<div style={{ color: "#6e7681" }}>{`// Requête entrante`}</div>
<div><span style={{ color: "#79c0ff" }}>POST</span> <span style={{ color: "#a5d6ff" }}>/v1/chat/completions</span></div>
<div><span style={{ color: dim }}>tenant:</span> <span style={{ color: brandLight }}>acme-corp</span> · <span style={{ color: dim }}>user:</span> <span style={{ color: brandLight }}>alice</span></div>
<br />
<div style={{ color: "#6e7681" }}>{`// Détection PII — 12ms`}</div>
<div><span style={{ color: "#e3b341" }}> </span><span style={{ color: "#79c0ff" }}>PERSON&nbsp;&nbsp;&nbsp; </span><span style={{ color: "#f85149" }}>"Marie Dubois"</span><span style={{ color: dim }}> </span><span style={{ color: "#fbbf24", background: "rgba(251,191,36,.1)", padding: "0 3px", borderRadius: 3 }}>[PERSONNE_1]</span></div>
<div><span style={{ color: "#e3b341" }}> </span><span style={{ color: "#79c0ff" }}>IBAN&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style={{ color: "#f85149" }}>"FR76 3000..."</span><span style={{ color: dim }}> </span><span style={{ color: "#fbbf24", background: "rgba(251,191,36,.1)", padding: "0 3px", borderRadius: 3 }}>[IBAN_1]</span></div>
<div><span style={{ color: "#e3b341" }}> </span><span style={{ color: "#79c0ff" }}>EMAIL&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</span><span style={{ color: "#f85149" }}>"m.dubois@..."</span><span style={{ color: dim }}> </span><span style={{ color: "#fbbf24", background: "rgba(251,191,36,.1)", padding: "0 3px", borderRadius: 3 }}>[EMAIL_1]</span></div>
<br />
<div style={{ color: "#6e7681" }}>{`// Routage intelligent — règle #3`}</div>
<div><span style={{ color: "#56d364" }}> </span>provider <span style={{ color: brandLight }}>azure</span> · model <span style={{ color: brandLight }}>gpt-4o</span></div>
<div><span style={{ color: "#56d364" }}> </span>dept <span style={{ color: brandLight }}>finance</span> · budget OK</div>
<br />
<div style={{ color: "#6e7681" }}>{`// Log d'audit — ClickHouse`}</div>
<div><span style={{ color: "#56d364" }}> </span><span style={{ color: dim }}>tokens: </span><span style={{ color: "#a5d6ff" }}>847 in / 312 out</span></div>
<div><span style={{ color: "#56d364" }}> </span><span style={{ color: dim }}>coût: </span><span style={{ color: "#a5d6ff" }}>0.0043 imputé</span></div>
<div><span style={{ color: "#56d364" }}> </span><span style={{ color: dim }}>latence totale: </span><span style={{ color: "#a5d6ff" }}>38ms </span></div>
</div>
</div>
</div>
</section>
{/* TRUST BAR */}
<div style={{ position: "relative", zIndex: 1, padding: "1.75rem 1.5rem", borderTop: `1px solid ${border}`, borderBottom: `1px solid ${border}`, background: "rgba(255,255,255,.015)" }}>
<div style={{ maxWidth: 1200, margin: "0 auto", display: "flex", alignItems: "center", gap: "2.5rem", flexWrap: "wrap", justifyContent: "center" }}>
<span style={{ fontSize: ".72rem", color: dim, fontWeight: 600, textTransform: "uppercase", letterSpacing: ".08em" }}>Compatible avec</span>
{["OpenAI", "Anthropic", "Azure OpenAI", "Mistral AI", "Ollama (on-premise)"].map((p) => (
<span key={p} style={{ color: dim, fontSize: ".82rem", fontWeight: 600, opacity: 0.6 }}>{p}</span>
))}
</div>
</div>
{/* PROBLEM */}
<section id="probleme" style={{ padding: "6rem 1.5rem", position: "relative", zIndex: 1 }}>
<div style={{ maxWidth: 1200, margin: "0 auto" }}>
<Label>Le problème</Label>
<h2 style={{ fontSize: "clamp(1.75rem,3.5vw,2.6rem)", fontWeight: 700, lineHeight: 1.2, letterSpacing: "-0.025em" }}>Vos équipes utilisent l'IA.<br />Vous n'en savez rien.</h2>
<p style={{ marginTop: "1rem", maxWidth: 560, lineHeight: 1.7, color: muted }}>ChatGPT, Claude, Copilot vos collaborateurs contournent les politiques IT. Résultat : données sensibles exposées, coûts incontrôlés, conformité compromise.</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: "1.5rem", marginTop: "3rem" }}>
{problems.map((p) => (
<div key={p.title} style={{ background: "rgba(239,68,68,.05)", border: "1px solid rgba(239,68,68,.15)", borderRadius: 20, padding: "2rem" }}>
<div style={{ fontSize: "1.4rem", marginBottom: "1.25rem" }}>{p.icon}</div>
<h3 style={{ fontWeight: 600, marginBottom: ".5rem" }}>{p.title}</h3>
<p style={{ color: muted, fontSize: ".875rem", lineHeight: 1.65 }}>{p.body}</p>
</div>
))}
</div>
</div>
</section>
{/* FEATURES */}
<section id="fonctionnalites" style={{ padding: "6rem 1.5rem", borderTop: `1px solid ${border}`, position: "relative", zIndex: 1 }}>
<div style={{ maxWidth: 1200, margin: "0 auto" }}>
<Label>La solution</Label>
<h2 style={{ fontSize: "clamp(1.75rem,3.5vw,2.6rem)", fontWeight: 700, lineHeight: 1.2, letterSpacing: "-0.025em" }}>Un proxy intelligent entre<br />vos équipes et les LLMs</h2>
<p style={{ marginTop: "1rem", maxWidth: 560, lineHeight: 1.7, color: muted }}>Veylant se déploie en 15 minutes sans modifier votre code existant. Il intercepte chaque requête et applique vos politiques en temps réel.</p>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: "1.5rem", marginTop: "3rem" }}>
{features.map((f) => (
<div key={f.title} style={{ background: card, border: `1px solid ${border}`, borderRadius: 20, padding: "2rem" }}>
<div style={{ width: 46, height: 46, background: "rgba(79,70,229,.15)", borderRadius: 11, display: "flex", alignItems: "center", justifyContent: "center", fontSize: "1.3rem", marginBottom: "1.25rem" }}>{f.icon}</div>
<h3 style={{ fontWeight: 600, marginBottom: ".45rem" }}>{f.title}</h3>
<p style={{ color: muted, fontSize: ".875rem", lineHeight: 1.65 }}>{f.body}</p>
<span style={{ display: "inline-block", marginTop: ".9rem", fontSize: ".72rem", fontWeight: 700, color: brandLight, background: "rgba(79,70,229,.15)", padding: ".2rem .65rem", borderRadius: "100px" }}>{f.badge}</span>
</div>
))}
</div>
</div>
</section>
{/* SECURITY */}
<section id="securite" style={{ padding: "6rem 1.5rem", borderTop: `1px solid ${border}`, position: "relative", zIndex: 1 }}>
<div style={{ maxWidth: 1200, margin: "0 auto" }}>
<Label>Sécurité</Label>
<h2 style={{ fontSize: "clamp(1.75rem,3.5vw,2.6rem)", fontWeight: 700, lineHeight: 1.2, letterSpacing: "-0.025em" }}>Conçu pour les équipes<br />sécurité les plus exigeantes</h2>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "3rem", alignItems: "center", marginTop: "3rem" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
{[
{ icon: "🔐", title: "Zero Trust & mTLS", body: "Communication inter-services chiffrée via mTLS. TLS 1.3 en externe. Aucun trafic en clair, jamais." },
{ icon: "🔑", title: "Chiffrement bout-en-bout", body: "Prompts chiffrés AES-256-GCM au repos. Clés API en SHA-256. Rotation 90 jours via HashiCorp Vault." },
{ icon: "✅", title: "Pentest réussi — 2026", body: "0 vulnérabilité critique, 0 high. Semgrep SAST + Trivy image scanning + OWASP ZAP DAST en CI/CD." },
{ icon: "📝", title: "Audit de l'audit", body: "Chaque accès aux logs d'audit est lui-même loggué. Traçabilité complète et inviolable." },
].map((c) => (
<div key={c.title} style={{ display: "flex", alignItems: "flex-start", gap: "1rem", padding: "1.25rem", background: card, border: `1px solid ${border}`, borderRadius: 12 }}>
<div style={{ width: 36, height: 36, background: "rgba(16,185,129,.1)", borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0, fontSize: ".95rem" }}>{c.icon}</div>
<div>
<h4 style={{ fontWeight: 600, fontSize: ".875rem", marginBottom: ".2rem" }}>{c.title}</h4>
<p style={{ fontSize: ".8rem", color: muted, lineHeight: 1.55 }}>{c.body}</p>
</div>
</div>
))}
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
{[
{ ico: "🇪🇺", title: "RGPD natif", body: "Registre Art. 30 automatique. Notification CNIL prête sous 72h." },
{ ico: "⚖️", title: "EU AI Act", body: "Classification des risques, documentation système requise." },
{ ico: "🏛️", title: "NIS2 ready", body: "Logs immuables, alertes PagerDuty, SLO 99,5 %." },
{ ico: "🔒", title: "ISO 27001", body: "Architecture Zero Trust, RBAC, gestion des secrets." },
].map((b) => (
<div key={b.title} style={{ background: card, border: `1px solid ${border}`, borderRadius: 20, padding: "1.5rem", textAlign: "center" }}>
<div style={{ fontSize: "1.9rem", marginBottom: ".75rem" }}>{b.ico}</div>
<h4 style={{ fontWeight: 700, fontSize: ".875rem", marginBottom: ".2rem" }}>{b.title}</h4>
<p style={{ fontSize: ".75rem", color: muted }}>{b.body}</p>
</div>
))}
</div>
</div>
</div>
</section>
{/* PERSONAS */}
<section id="personas" style={{ padding: "6rem 1.5rem", borderTop: `1px solid ${border}`, position: "relative", zIndex: 1 }}>
<div style={{ maxWidth: 1200, margin: "0 auto" }}>
<Label>Pour qui</Label>
<h2 style={{ fontSize: "clamp(1.75rem,3.5vw,2.6rem)", fontWeight: 700, lineHeight: 1.2, letterSpacing: "-0.025em" }}>Un outil, trois interlocuteurs</h2>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: "1.5rem", marginTop: "3rem" }}>
{personas.map((p) => (
<div key={p.role} style={{ background: card, border: `1px solid ${border}`, borderRadius: 20, padding: "2rem" }}>
<div style={{ fontSize: ".7rem", fontWeight: 800, textTransform: "uppercase", letterSpacing: ".12em", color: brandLight, marginBottom: ".2rem" }}>{p.role}</div>
<div style={{ fontSize: "1.05rem", fontWeight: 700, marginBottom: "1.1rem" }}>{p.title}</div>
<ul style={{ listStyle: "none", display: "flex", flexDirection: "column", gap: ".55rem" }}>
{p.items.map((item) => (
<li key={item} style={{ fontSize: ".84rem", color: muted, display: "flex", gap: ".5rem", alignItems: "flex-start" }}>
<span style={{ color: brandLight, flexShrink: 0 }}></span>
{item}
</li>
))}
</ul>
</div>
))}
</div>
</div>
</section>
{/* CTA */}
<section id="contact" style={{ padding: "6rem 1.5rem", textAlign: "center", background: "radial-gradient(ellipse at center, rgba(79,70,229,.14) 0%, transparent 65%)", position: "relative", zIndex: 1 }}>
<div style={{ maxWidth: 1200, margin: "0 auto" }}>
<Label>Commencer</Label>
<h2 style={{ fontSize: "clamp(1.75rem,3.5vw,2.6rem)", fontWeight: 700, lineHeight: 1.2, letterSpacing: "-0.025em", marginBottom: "1rem" }}>Prêt à reprendre le contrôle<br />de votre IA d'entreprise ?</h2>
<p style={{ color: muted, fontSize: "1.05rem", marginBottom: "2.5rem" }}>Démo personnalisée · Déploiement en 15 minutes · Support dédié</p>
<div style={{ display: "flex", gap: "1rem", justifyContent: "center", flexWrap: "wrap" }}>
<BtnPrimary href="mailto:demo@veylant.io">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
Demander une démo
</BtnPrimary>
<BtnOutline href={GITHUB_URL} target="_blank" rel="noopener noreferrer">
<svg width="15" height="15" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2z"/></svg>
Voir sur GitHub
</BtnOutline>
<BtnOutline to="/docs">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>
Documentation
</BtnOutline>
</div>
</div>
</section>
{/* FOOTER */}
<footer style={{ position: "relative", zIndex: 1, borderTop: `1px solid ${border}`, padding: "2.5rem 1.5rem" }}>
<div style={{ maxWidth: 1200, margin: "0 auto", display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: "1rem" }}>
<Link to="/" style={{ fontSize: "1.25rem", fontWeight: 800, color: "#f1f5f9", textDecoration: "none" }}>Veylant<span style={{ color: brandLight }}> IA</span></Link>
<div style={{ display: "flex", gap: "1.5rem" }}>
<Link to="/docs" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Documentation</Link>
<a href={PLAYGROUND_URL} style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Playground</a>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>GitHub</a>
<a href="mailto:demo@veylant.io" style={{ fontSize: ".84rem", color: dim, textDecoration: "none" }}>Contact</a>
</div>
<span style={{ fontSize: ".8rem", color: dim }}>© 2026 Veylant. Conçu pour l'entreprise européenne.</span>
</div>
</footer>
</div>
);
}

80
web-public/src/router.tsx Normal file
View File

@ -0,0 +1,80 @@
import { createBrowserRouter } from "react-router-dom";
import { LandingPage } from "@/pages/LandingPage";
// Documentation site — shared pages, zero code duplication
import { DocLayout } from "@docs/DocLayout";
import { DocsHomePage } from "@docs/DocsHomePage";
import { WhatIsVeylantPage } from "@docs/getting-started/WhatIsVeylantPage";
import { QuickStartPage } from "@docs/getting-started/QuickStartPage";
import { KeyConceptsPage } from "@docs/getting-started/KeyConceptsPage";
import { DockerComposePage } from "@docs/installation/DockerComposePage";
import { ConfigurationPage } from "@docs/installation/ConfigurationPage";
import { ProvidersPage } from "@docs/installation/ProvidersPage";
import { AuthenticationPage } from "@docs/api-reference/AuthenticationPage";
import { ChatCompletionsPage } from "@docs/api-reference/ChatCompletionsPage";
import { PiiAnalysisPage } from "@docs/api-reference/PiiAnalysisPage";
import { AdminPoliciesPage } from "@docs/api-reference/AdminPoliciesPage";
import { AdminUsersPage } from "@docs/api-reference/AdminUsersPage";
import { AdminLogsPage } from "@docs/api-reference/AdminLogsPage";
import { AdminCompliancePage } from "@docs/api-reference/AdminCompliancePage";
import { AdminFlagsPage } from "@docs/api-reference/AdminFlagsPage";
import { PiiGuide } from "@docs/guides/PiiGuide";
import { RoutingGuide } from "@docs/guides/RoutingGuide";
import { RbacGuide } from "@docs/guides/RbacGuide";
import { ComplianceGuide } from "@docs/guides/ComplianceGuide";
import { MonitoringGuide } from "@docs/guides/MonitoringGuide";
import { CircuitBreakerGuide } from "@docs/guides/CircuitBreakerGuide";
import { DockerPage } from "@docs/deployment/DockerPage";
import { KubernetesPage } from "@docs/deployment/KubernetesPage";
import { BlueGreenPage } from "@docs/deployment/BlueGreenPage";
import { SecurityModelPage } from "@docs/security/SecurityModelPage";
import { ApiKeysPage } from "@docs/security/ApiKeysPage";
import { ChangelogPage } from "@docs/ChangelogPage";
export const router = createBrowserRouter([
{
path: "/",
element: <LandingPage />,
},
// Documentation site — public, no auth
{
path: "/docs",
element: <DocLayout />,
children: [
{ index: true, element: <DocsHomePage /> },
// Getting Started
{ path: "getting-started/what-is-veylant", element: <WhatIsVeylantPage /> },
{ path: "getting-started/quick-start", element: <QuickStartPage /> },
{ path: "getting-started/concepts", element: <KeyConceptsPage /> },
// Installation
{ path: "installation/docker", element: <DockerComposePage /> },
{ path: "installation/configuration", element: <ConfigurationPage /> },
{ path: "installation/providers", element: <ProvidersPage /> },
// API Reference
{ path: "api/authentication", element: <AuthenticationPage /> },
{ path: "api/chat-completions", element: <ChatCompletionsPage /> },
{ path: "api/pii", element: <PiiAnalysisPage /> },
{ path: "api/admin/policies", element: <AdminPoliciesPage /> },
{ path: "api/admin/users", element: <AdminUsersPage /> },
{ path: "api/admin/logs", element: <AdminLogsPage /> },
{ path: "api/admin/compliance", element: <AdminCompliancePage /> },
{ path: "api/admin/flags", element: <AdminFlagsPage /> },
// Guides
{ path: "guides/pii", element: <PiiGuide /> },
{ path: "guides/routing", element: <RoutingGuide /> },
{ path: "guides/rbac", element: <RbacGuide /> },
{ path: "guides/compliance", element: <ComplianceGuide /> },
{ path: "guides/monitoring", element: <MonitoringGuide /> },
{ path: "guides/circuit-breaker", element: <CircuitBreakerGuide /> },
// Deployment
{ path: "deployment/docker", element: <DockerPage /> },
{ path: "deployment/kubernetes", element: <KubernetesPage /> },
{ path: "deployment/blue-green", element: <BlueGreenPage /> },
// Security
{ path: "security/model", element: <SecurityModelPage /> },
{ path: "security/api-keys", element: <ApiKeysPage /> },
// Changelog
{ path: "changelog", element: <ChangelogPage /> },
],
},
]);

59
web-public/src/styles.css Normal file
View File

@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

10
web-public/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_DASHBOARD_URL: string;
readonly VITE_PLAYGROUND_URL: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}

View File

@ -0,0 +1,63 @@
import type { Config } from "tailwindcss";
const config: Config = {
darkMode: ["class"],
// Include both local sources AND the shared doc pages from the main web app
content: [
"./index.html",
"./src/**/*.{ts,tsx}",
"../web/src/pages/docs/**/*.{ts,tsx}",
],
theme: {
container: {
center: true,
padding: "2rem",
screens: { "2xl": "1400px" },
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [require("@tailwindcss/typography")],
};
export default config;

25
web-public/tsconfig.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"],
"@docs/*": ["../web/src/pages/docs/*"]
}
},
"include": ["src", "../web/src/pages/docs"]
}

18
web-public/vite.config.ts Normal file
View File

@ -0,0 +1,18 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
// Local sources
"@": path.resolve(__dirname, "./src"),
// Shared doc pages — live in the main web app, never duplicated
"@docs": path.resolve(__dirname, "../web/src/pages/docs"),
},
},
server: {
port: 3001,
},
});