This commit is contained in:
David 2026-03-13 13:16:35 +01:00
parent 279e8f88c3
commit 7c5d9d1225
18 changed files with 4096 additions and 45 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Veylant IA Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

508
README.md
View File

@ -1,66 +1,486 @@
<div align="center">
<img src="https://img.shields.io/badge/version-1.0.0-blue?style=for-the-badge" alt="Version 1.0.0">
<img src="https://img.shields.io/badge/Go-1.24-00ADD8?style=for-the-badge&logo=go" alt="Go 1.24">
<img src="https://img.shields.io/badge/Python-3.12-3776AB?style=for-the-badge&logo=python" alt="Python 3.12">
<img src="https://img.shields.io/badge/React-18-61DAFB?style=for-the-badge&logo=react" alt="React 18">
<img src="https://img.shields.io/badge/license-MIT-green?style=for-the-badge" alt="MIT License">
<br/><br/>
# Veylant IA — AI Governance Hub
B2B SaaS platform acting as an intelligent proxy/gateway for enterprise AI consumption.
Prevents Shadow AI, enforces PII anonymization, ensures GDPR/EU AI Act compliance, and controls costs across all LLM usage.
**The enterprise intelligence layer between your teams and the LLMs.**
## Quick start
PII anonymization · Intelligent routing · GDPR/EU AI Act compliance · Cost control · Full audit trail
```bash
# Start the full local stack (proxy + PostgreSQL + ClickHouse + Redis + Keycloak)
make dev
[Documentation](https://github.com/DH7789-dev/Veylant-IA/wiki) · [Quick Start](#quick-start) · [Architecture](#architecture) · [Contributing](#contributing)
# Health check
make health
# → {"status":"ok","timestamp":"..."}
</div>
# Stop and clean
make dev-down
---
## Why Veylant IA?
Most organizations adopting AI face the same problems: employees using personal ChatGPT accounts with sensitive data, no visibility into what is sent to which model, no cost control, and zero compliance posture for GDPR or the EU AI Act.
Veylant IA solves this by acting as a **transparent reverse proxy** in front of every LLM your company uses. It intercepts every request, strips PII before it reaches any provider, routes traffic according to configurable policies, logs everything to an immutable audit trail, and gives compliance officers a one-click GDPR Article 30 report.
```
Your app / IDE / Slack bot
┌──────────────────────────────────────────────┐
│ Veylant IA Proxy │
│ Auth → PII Scan → Route → Audit → Bill │
└──────────────────────────────────────────────┘
│ │ │
OpenAI API Anthropic API Mistral / Ollama
```
## Test credentials (development only)
**Zero code change required.** Point your `OPENAI_BASE_URL` at the proxy — everything else stays the same.
| User | Password | Role |
|------|----------|------|
| admin@veylant.dev | admin123 | Admin |
| user@veylant.dev | user123 | User |
---
Keycloak admin console: http://localhost:8080 (admin / admin)
## Features
| Category | Capability |
|---|---|
| **Shadow AI Prevention** | Drop-in proxy; works with any OpenAI-compatible SDK |
| **PII Anonymization** | 3-layer detection: regex → Presidio NER → LLM validation; pseudonymization with Redis mapping |
| **Intelligent Routing** | Priority-based rules engine (JSONB conditions: role, department, sensitivity, model, token estimate) |
| **Fallback Chains** | Automatic failover across providers with circuit breaker (threshold=5, TTL=60s) |
| **GDPR Compliance** | Art.30 registry, Art.15 access, Art.17 erasure, DPIA reports — all generated as PDF |
| **EU AI Act** | Risk classification (Minimal/Limited/High/Unacceptable) from a 5-question questionnaire |
| **Audit Logs** | Append-only ClickHouse storage; exportable as CSV; access-of-access logging |
| **RBAC** | 4 roles (admin, manager, user, auditor); per-model and per-department permissions |
| **Cost Tracking** | Token-level billing per provider; budget alerts by email |
| **Rate Limiting** | Token-bucket per tenant + per user; DB overrides without restart |
| **Multi-tenancy** | PostgreSQL Row-Level Security; logical isolation with no data bleed |
| **Streaming** | Full SSE pass-through; PII applied to request, not streamed response |
| **Provider Hot-reload** | Add/update/remove LLM providers from the admin UI without restarting the proxy |
| **Observability** | Prometheus metrics, Grafana dashboards, SLO 99.5%, 7 alerting rules (PagerDuty + Slack) |
---
## Architecture
See `docs/AI_Governance_Hub_PRD.md` for the full technical architecture.
```
API Gateway (Traefik)
┌─────────────────────────────────────────────────────┐
│ Go Proxy [cmd/proxy] │
│ chi · zap · viper · HS256 JWT · distroless image │
│ │
Client request │ ┌───────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
──────────────────────► │ │ Auth │→ │ Rate Lim │→ │ Router │→ │ PII │ │
OpenAI-compatible │ └───────┘ └──────────┘ └──────────┘ └──┬───┘ │
│ │ │
│ ┌─────────────────────────────────────────▼────┐ │
│ │ Provider Dispatch + Fallback │ │
│ │ OpenAI · Anthropic · Azure · Mistral · Ollama│ │
│ └─────────────────────────────────────────┬────┘ │
│ │ │
│ ┌──────────┐ ┌──────────┐ ┌────────────▼────┐ │
│ │ Billing │ │ Metrics │ │ Audit Logger │ │
│ └──────────┘ └──────────┘ └─────────────────┘ │
└─────────────────────────────────────────────────────┘
│ gRPC (<2ms) async batch
▼ ▼
┌───────────────────────┐ ┌─────────────────┐
│ PII Service │ │ ClickHouse │
│ FastAPI + grpc.aio │ │ (append-only) │
│ Regex → Presidio NER │ └─────────────────┘
│ → LLM validation │
└───────────────────────┘
Go Proxy [cmd/proxy] ← chi router, JWT auth, routing rules
├── Module Auth ← Keycloak/OIDC/SAML
├── Module Router ← rules engine
├── Module Logger ← ClickHouse append-only
├── Module PII ← gRPC → Python sidecar
├── Module Billing ← cost tracking
└── Module RBAC ← row-level per tenant
│ gRPC
PII Service [services/pii] ← FastAPI + Presidio + spaCy
LLM Adapters ← OpenAI, Anthropic, Azure, Mistral, Ollama
┌────────────────┼────────────────┐
▼ ▼ ▼
PostgreSQL 16 Redis 7 Prometheus
(RLS tenancy) (rate limit, + Grafana
PII mapping)
```
## Commands
**Stack at a glance:**
| Layer | Technology |
|---|---|
| Proxy | Go 1.24, chi, zap, viper |
| PII sidecar | Python 3.12, FastAPI, Presidio, spaCy fr_core_news_lg |
| Relational DB | PostgreSQL 16 with Row-Level Security |
| Analytics | ClickHouse (append-only audit logs, TTL retention) |
| Cache / sessions | Redis 7, AES-256-GCM encrypted mappings |
| Frontend | React 18, TypeScript, Vite, shadcn/ui, Recharts |
| Observability | Prometheus, Grafana, Alertmanager |
| Secrets | HashiCorp Vault (90-day API key rotation) |
| Infra | Helm + Kubernetes (EKS), Terraform, Istio blue/green |
---
## Quick Start
### Prerequisites
- Docker + Docker Compose
- Go 1.24+ (for local development)
- `buf` (`brew install buf`) — for proto regeneration only
### 1. Clone and start
```bash
make build # go build ./cmd/proxy/
make test # go test -race ./...
make lint # golangci-lint + black --check
make fmt # gofmt + black
make proto # buf generate (requires: brew install buf)
make migrate-up # apply DB migrations
make health # curl /healthz
git clone https://github.com/DH7789-dev/Veylant-IA.git
cd Veylant-IA
# Copy the example config
cp config.yaml.example config.yaml # or use the default config.yaml
# Start the full local stack
# (PostgreSQL · ClickHouse · Redis · PII service · Proxy · Prometheus · Grafana · React dashboard)
make dev
```
## Documentation
The first start downloads ~2 GB of images and model data. Subsequent starts take ~10 seconds.
- `docs/AI_Governance_Hub_PRD.md` — Full product requirements
- `docs/AI_Governance_Hub_Plan_Realisation.md` — 26-week execution plan (164 tasks)
- `docs/Veylant_IA_Plan_Agile_Scrum.md` — Agile/Scrum plan (13 sprints)
- `docs/adr/` — Architecture Decision Records
### 2. Verify
```bash
make health
# → {"status":"ok","timestamp":"2026-01-01T00:00:00Z","version":"1.0.0"}
```
### 3. Open the dashboard
| Service | URL | Credentials |
|---|---|---|
| Dashboard | http://localhost:3000 | admin@veylant.dev / admin123 |
| Playground | http://localhost:8090/playground | — (public) |
| Documentation | http://localhost:3000/docs | — (public) |
| Grafana | http://localhost:3001 | admin / admin |
| Prometheus | http://localhost:9090 | — |
### 4. Send your first proxied request
```bash
# Obtain a JWT
TOKEN=$(curl -s -X POST http://localhost:8090/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@veylant.dev","password":"admin123"}' \
| jq -r '.token')
# Send a request — identical to the OpenAI API
curl http://localhost:8090/v1/chat/completions \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{"role":"user","content":"Mon IBAN est FR7614508 — peux-tu m'\''aider?"}]
}'
```
The proxy will strip `FR7614508` before sending it upstream and return the response with the pseudonymized token.
### 5. Use with any OpenAI-compatible SDK
```python
from openai import OpenAI
import httpx, json
# Get a JWT
resp = httpx.post("http://localhost:8090/v1/auth/login",
json={"email": "admin@veylant.dev", "password": "admin123"})
token = resp.json()["token"]
# Point the OpenAI SDK at Veylant IA
client = OpenAI(
api_key=token,
base_url="http://localhost:8090/v1",
)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Hello from Veylant IA!"}],
)
print(response.choices[0].message.content)
```
---
## Configuration
All configuration lives in `config.yaml`. Every key can be overridden via environment variable using the `VEYLANT_` prefix with `.` replaced by `_`.
```yaml
server:
port: 8090
env: development # "production" → fatal on any missing service
tenant_name: "Acme Corp"
auth:
jwt_secret: "change-me-in-production"
jwt_ttl_hours: 24
pii:
grpc_addr: "localhost:50051"
timeout_ms: 100
fail_open: true # false in production
notifications:
smtp:
host: "smtp.example.com"
port: 587
username: "alerts@example.com"
password: "..."
from: "alerts@example.com"
from_name: "Veylant IA"
```
```bash
# Environment variable override example
VEYLANT_AUTH_JWT_SECRET=my-secret \
VEYLANT_SERVER_ENV=production \
./bin/proxy
```
---
## Development
```bash
# Build
make build # → bin/proxy
# Test
make test # go test -race ./...
make test-cover # HTML coverage report → coverage.html
make test-integration # testcontainers (requires Docker)
# Single test
go test -run TestRuleEngine ./internal/routing/
pytest services/pii/tests/test_regex.py::test_iban
# Code quality
make lint # golangci-lint + black --check + ruff check
make fmt # gofmt + black
make check # Full pre-commit: build + vet + lint + test
# Frontend
cd web && npm install && npm run dev # Vite dev server on :3000 with HMR
cd web && npm run build # Production build → web/dist/
cd web && npm run lint # ESLint (max-warnings: 0)
# Database
make migrate-up # Apply pending migrations
make migrate-down # Roll back last migration
make migrate-status # Show current version
# Proto (only needed when editing .proto files)
make proto # buf generate → gen/ and services/pii/gen/
make proto-lint # buf lint
```
### Development mode graceful degradation
When `server.env=development`, the proxy starts even if services are unavailable:
- PostgreSQL unreachable → routing disabled, feature flags use in-memory fallback
- ClickHouse unreachable → audit logging uses in-memory `MemLogger`
- PII service unreachable → PII disabled if `pii.fail_open=true`
In production mode, any unavailable service causes a fatal startup error.
---
## API Reference
The proxy exposes a fully documented REST API. All endpoints return errors in OpenAI JSON format.
| Group | Endpoints |
|---|---|
| **Auth** | `POST /v1/auth/login` |
| **Proxy** | `POST /v1/chat/completions` (streaming supported) |
| **PII** | `POST /v1/pii/analyze` |
| **Admin — Logs** | `GET /v1/admin/logs`, `GET /v1/admin/compliance/export/logs` |
| **Admin — Users** | `GET/POST /v1/admin/users`, `PUT/DELETE /v1/admin/users/{id}` |
| **Admin — Providers** | `GET/POST /v1/admin/providers`, `PUT/DELETE/POST-test /v1/admin/providers/{id}` |
| **Admin — Rules** | `GET/POST /v1/admin/routing-rules`, `PUT/DELETE /v1/admin/routing-rules/{id}` |
| **Admin — Rate Limits** | `GET/POST /v1/admin/rate-limits`, `PUT/DELETE /v1/admin/rate-limits/{id}` |
| **Admin — Flags** | `GET/POST /v1/admin/flags`, `PUT /v1/admin/flags/{key}` |
| **Compliance** | `GET/POST /v1/admin/compliance/entries`, `PUT/DELETE /v1/admin/compliance/entries/{id}` |
| **Compliance — GDPR** | `GET /v1/admin/compliance/report/article30` (PDF/JSON), `POST /v1/admin/compliance/gdpr/access`, `DELETE /v1/admin/compliance/gdpr/erasure` |
| **Compliance — AI Act** | `POST /v1/admin/compliance/classify`, `GET /v1/admin/compliance/report/aiact`, `GET /v1/admin/compliance/dpia/{id}` |
| **Notifications** | `POST /v1/notifications/send` |
Interactive docs (Swagger UI): http://localhost:8090/docs
Raw OpenAPI spec: http://localhost:8090/docs/openapi.yaml
---
## Deployment
### Docker Compose (single server)
```bash
# Production-like stack on a single machine
docker compose -f docker-compose.yml up -d
# Set secrets via environment
VEYLANT_AUTH_JWT_SECRET=your-secret \
VEYLANT_DATABASE_DSN=postgres://... \
docker compose up -d
```
### Kubernetes + Helm
```bash
# Staging deploy
IMAGE_TAG=1.0.0 KUBECONFIG=~/.kube/config make helm-deploy
# Blue/green production deploy
make deploy-blue IMAGE_TAG=1.1.0 # Deploy to blue slot
make deploy-green IMAGE_TAG=1.1.0 # Switch traffic to green
make deploy-rollback ACTIVE_SLOT=blue # Instant rollback (<5s)
```
Helm chart is published to GHCR OCI:
```bash
helm install veylant-proxy oci://ghcr.io/DH7789-dev/charts/veylant-proxy --version 1.0.0
```
### Terraform (AWS EKS)
```bash
cd deploy/terraform
terraform init
terraform plan -var="cluster_name=veylant-prod" -var="region=eu-west-3"
terraform apply
```
The Terraform module provisions: EKS v1.31 (3-AZ node groups), RDS PostgreSQL, ElastiCache Redis, S3 backup bucket with IRSA, and configures Istio for blue/green traffic management.
### Public site (Landing page + Documentation)
The standalone `web-public/` app can be deployed independently:
```bash
# Build
docker build -f web-public/Dockerfile \
--build-arg VITE_DASHBOARD_URL=https://app.veylant.io \
--build-arg VITE_PLAYGROUND_URL=https://proxy.veylant.io/playground \
-t veylant-public .
# Portainer stack — see web-public/docker-compose.yml
```
---
## Security
Veylant IA was designed with a Zero Trust security model and underwent a grey-box penetration test (2026-06-09→20) with **0 Critical, 0 High** findings.
| Control | Implementation |
|---|---|
| Transport | TLS 1.3 external, mTLS between services |
| Authentication | HS256 JWT, bcrypt password hashing |
| Authorization | RBAC with PostgreSQL Row-Level Security |
| Secrets | AES-256-GCM at application level; API keys stored as SHA-256 hashes |
| API keys | HashiCorp Vault, 90-day rotation cycle |
| Audit | Every request logged; access to audit logs is itself logged |
| SAST | Semgrep rules enforced in CI (SQL injection, context propagation, sensitive field logging) |
| Container scan | Trivy (CRITICAL/HIGH blocking) |
| Secrets detection | gitleaks in CI |
| DAST | OWASP ZAP (non-blocking, main branch only) |
**Responsible disclosure:** Please report security vulnerabilities by opening a private advisory on GitHub or emailing security@veylant.io.
---
## Observability
- **Metrics**: Prometheus scrapes the proxy on `:9090`; 7 pre-built alerting rules cover latency, error rate, circuit breaker state, certificate expiry, DB connections, and PII anomalies.
- **Dashboards**: Two Grafana dashboards — `proxy-overview.json` (operational) and `production-slo.json` (SLO 99.5%, error budget burn rate).
- **Alerting**: PagerDuty for `critical` severity; Slack for `warning`.
- **Load testing**: k6 scenarios (`smoke` / `load` / `stress` / `soak`) — run with `make load-test SCENARIO=load`.
---
## Tenant Onboarding
```bash
# After `make dev`, seed a new tenant with default routing rules and rate limits
./deploy/onboarding/onboard-tenant.sh
# Bulk import users from CSV (email, first_name, last_name, department, role)
./deploy/onboarding/import-users.sh users.csv
```
---
## Project Structure
```
cmd/proxy/ Go entry point — wires all modules, starts HTTP server
internal/ Go modules (auth, middleware, router, pii, auditlog, compliance,
admin, billing, circuitbreaker, ratelimit, flags, crypto,
metrics, provider, proxy, apierror, health, notifications, config)
gen/ Generated gRPC stubs (buf generate — never edit manually)
services/pii/ Python FastAPI + gRPC PII detection service
proto/pii/v1/ gRPC .proto definitions
migrations/ golang-migrate SQL files (up/down pairs)
clickhouse/ ClickHouse DDL applied at startup
web/ React 18 dashboard (Vite, shadcn/ui)
src/pages/docs/ Public documentation site (37 pages, shared with web-public)
web-public/ Standalone React app: landing page + docs (separate build)
test/integration/ Integration tests (testcontainers-go, //go:build integration)
test/k6/ k6 load test scripts (smoke/load/stress/soak)
deploy/ Helm, Kubernetes, Terraform, Prometheus, Grafana, Alertmanager
onboarding/ Tenant seed scripts
docs/ PRD, execution plan, ADRs, runbooks, commercial docs
CHANGELOG.md Full version history
```
---
## Contributing
Contributions are welcome! Please read through the guidelines below before opening a PR.
### Development workflow
1. Fork the repository and create a feature branch from `main`
2. Run `make check` before committing — this runs build, vet, lint, and tests
3. Follow [Conventional Commits](https://www.conventionalcommits.org/): `feat:`, `fix:`, `chore:`, `docs:`
4. Ensure Go internal packages maintain ≥80% test coverage; Python PII service ≥75%
5. Integration tests (`//go:build integration`) must pass — they use testcontainers and require Docker
6. Open a pull request against `main` — CI runs automatically
### Code style
- **Go**: `goimports` with local prefix `github.com/veylant/ia-gateway`; three import groups (stdlib · external · internal)
- **Python**: `black` + `ruff`; no `eval()` or `exec()` on external data
- **React**: ESLint with max-warnings: 0; UI copy in French; `date-fns` with `fr` locale
### Custom Semgrep rules
CI enforces project-specific SAST rules:
- No `context.Background()` in HTTP handlers → use `r.Context()`
- No SQL string concatenation → use parameterized queries
- No sensitive fields in structured logs → use redaction helpers
- No hardcoded API keys (strings starting with `sk-`)
- `json.NewDecoder(r.Body)` must be preceded by `http.MaxBytesReader`
### Adding a new LLM provider
Implement the `provider.Adapter` interface (`Send()`, `Stream()`, `Validate()`, `HealthCheck()`) in `internal/provider/<name>/`. Add the provider type to the factory in `internal/admin/provider_configs.go` and register it in the Helm chart's allowed providers list.
---
## License
MIT © 2026 Veylant IA — see [LICENSE](LICENSE) for details.
---
<div align="center">
Built with Go · Python · React &nbsp;|&nbsp; Made in France 🇫🇷
[GitHub](https://github.com/DH7789-dev/Veylant-IA) · [Documentation](http://localhost:3000/docs) · [Report a bug](https://github.com/DH7789-dev/Veylant-IA/issues)
</div>

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,
},
});