Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ce1752aed | ||
|
|
11cffbcbb5 |
48
.env.example
Normal file
48
.env.example
Normal file
@ -0,0 +1,48 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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=
|
||||
59
.gitignore
vendored
59
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
# Go
|
||||
# ─── Go ───────────────────────────────────────────────────────────────────────
|
||||
bin/
|
||||
*.exe
|
||||
*.exe~
|
||||
@ -9,15 +9,13 @@ bin/
|
||||
*.out
|
||||
coverage.out
|
||||
coverage.html
|
||||
|
||||
# Vendor
|
||||
vendor/
|
||||
|
||||
# Go workspace
|
||||
coverage_internal.out
|
||||
coverage/
|
||||
go.work
|
||||
go.work.sum
|
||||
vendor/
|
||||
|
||||
# Python
|
||||
# ─── Python ───────────────────────────────────────────────────────────────────
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
@ -25,20 +23,27 @@ __pycache__/
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
htmlcov/
|
||||
.ruff_cache/
|
||||
|
||||
# Node / Frontend
|
||||
# ─── Node / Frontend ──────────────────────────────────────────────────────────
|
||||
node_modules/
|
||||
.next/
|
||||
out/
|
||||
dist/
|
||||
*.local
|
||||
web/dist/
|
||||
web/.vite/
|
||||
|
||||
# Environment & secrets
|
||||
# ─── web-public (standalone public site — has its own build/deploy) ───────────
|
||||
web-public/
|
||||
|
||||
# ─── Secrets & config ─────────────────────────────────────────────────────────
|
||||
# Real config lives in config.yaml — use config.yaml.example as the template
|
||||
config.yaml
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
@ -49,31 +54,37 @@ dist/
|
||||
secrets/
|
||||
vault-tokens/
|
||||
|
||||
# Docker
|
||||
.docker/
|
||||
# ─── Generated proto stubs (regenerated via `make proto`) ─────────────────────
|
||||
gen/
|
||||
services/pii/gen/
|
||||
|
||||
# Terraform
|
||||
# ─── Terraform state ──────────────────────────────────────────────────────────
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
*.tfplan
|
||||
.terraform.lock.hcl
|
||||
|
||||
# IDE
|
||||
# ─── Docker ───────────────────────────────────────────────────────────────────
|
||||
.docker/
|
||||
|
||||
# ─── Logs & temp ──────────────────────────────────────────────────────────────
|
||||
*.log
|
||||
logs/
|
||||
tmp/
|
||||
*.tmp
|
||||
|
||||
# ─── Test / scratch files ─────────────────────────────────────────────────────
|
||||
test_smtp.go
|
||||
|
||||
# ─── IDE ──────────────────────────────────────────────────────────────────────
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Generated proto stubs
|
||||
gen/
|
||||
services/pii/gen/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Coverage reports
|
||||
coverage/
|
||||
# ─── Compiled proxy binary ────────────────────────────────────────────────────
|
||||
proxy
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
server:
|
||||
port: 8090
|
||||
shutdown_timeout_seconds: 30
|
||||
env: development
|
||||
tenant_name: "Mon Organisation"
|
||||
# CORS: origins allowed to call the proxy from a browser (React dashboard).
|
||||
# Override in production: VEYLANT_SERVER_ALLOWED_ORIGINS=https://dashboard.veylant.ai
|
||||
env: development # "production" → fatal on any missing service
|
||||
tenant_name: "My Organisation"
|
||||
# CORS: origins allowed to call the proxy from a browser.
|
||||
# Override in production: VEYLANT_SERVER_ALLOWED_ORIGINS=https://dashboard.example.com
|
||||
allowed_origins:
|
||||
- "http://localhost:3000"
|
||||
|
||||
@ -17,28 +17,32 @@ database:
|
||||
redis:
|
||||
url: "redis://localhost:6379"
|
||||
|
||||
# Local JWT authentication (email/password — replaces Keycloak).
|
||||
# Override jwt_secret in production via VEYLANT_AUTH_JWT_SECRET.
|
||||
# Local JWT authentication (email/password).
|
||||
# MUST be changed in production — use a long random secret.
|
||||
# Generate: openssl rand -hex 32
|
||||
# Override: VEYLANT_AUTH_JWT_SECRET=<your-secret>
|
||||
auth:
|
||||
jwt_secret: "change-me-in-production-use-VEYLANT_AUTH_JWT_SECRET"
|
||||
jwt_secret: "change-me-in-production"
|
||||
jwt_ttl_hours: 24
|
||||
|
||||
pii:
|
||||
enabled: true
|
||||
service_addr: "localhost:50051"
|
||||
timeout_ms: 100
|
||||
fail_open: true
|
||||
fail_open: true # set false in production
|
||||
|
||||
log:
|
||||
level: "info"
|
||||
format: "json"
|
||||
level: "info" # debug | info | warn | error
|
||||
format: "json" # json | console
|
||||
|
||||
# LLM provider adapters.
|
||||
# Sensitive values (API keys) must be injected via env vars — never hardcode them.
|
||||
# API keys MUST be injected via env vars — never hardcode them here.
|
||||
# Example: VEYLANT_PROVIDERS_OPENAI_API_KEY=sk-...
|
||||
# Provider configs can also be managed via the admin API (POST /v1/admin/providers).
|
||||
providers:
|
||||
openai:
|
||||
base_url: "https://api.openai.com/v1"
|
||||
# api_key: set via VEYLANT_PROVIDERS_OPENAI_API_KEY
|
||||
timeout_seconds: 30
|
||||
max_conns: 100
|
||||
|
||||
@ -54,8 +58,8 @@ providers:
|
||||
timeout_seconds: 30
|
||||
max_conns: 100
|
||||
# api_key: set via VEYLANT_PROVIDERS_AZURE_API_KEY
|
||||
# resource_name: set via VEYLANT_PROVIDERS_AZURE_RESOURCE_NAME (e.g. "my-azure-resource")
|
||||
# deployment_id: set via VEYLANT_PROVIDERS_AZURE_DEPLOYMENT_ID (e.g. "gpt-4o")
|
||||
# resource_name: set via VEYLANT_PROVIDERS_AZURE_RESOURCE_NAME
|
||||
# deployment_id: set via VEYLANT_PROVIDERS_AZURE_DEPLOYMENT_ID
|
||||
|
||||
mistral:
|
||||
base_url: "https://api.mistral.ai/v1"
|
||||
@ -69,10 +73,9 @@ providers:
|
||||
max_conns: 10
|
||||
|
||||
# Role-based access control for the provider router.
|
||||
# Controls which models each role can access.
|
||||
rbac:
|
||||
# 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 roles always have unrestricted access.
|
||||
# Models accessible to the "user" role (exact match or prefix).
|
||||
# admin and manager always have unrestricted access.
|
||||
user_allowed_models:
|
||||
- "gpt-4o-mini"
|
||||
- "gpt-3.5-turbo"
|
||||
@ -85,45 +88,44 @@ metrics:
|
||||
path: "/metrics"
|
||||
|
||||
# Intelligent routing engine.
|
||||
# Rules are stored in the routing_rules table and cached per tenant.
|
||||
routing:
|
||||
# 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
|
||||
|
||||
# ClickHouse audit log (Sprint 6).
|
||||
# ClickHouse audit log.
|
||||
# DSN: clickhouse://user:pass@host:9000/database
|
||||
# Set via env var: VEYLANT_CLICKHOUSE_DSN
|
||||
# Override: VEYLANT_CLICKHOUSE_DSN=clickhouse://...
|
||||
clickhouse:
|
||||
dsn: "clickhouse://veylant:veylant_dev@localhost:9000/veylant_logs"
|
||||
max_conns: 10
|
||||
dial_timeout_seconds: 5
|
||||
|
||||
# Cryptography settings.
|
||||
# AES-256-GCM key for encrypting prompt_anonymized in the audit log.
|
||||
# MUST be set via env var in production: VEYLANT_CRYPTO_AES_KEY_BASE64
|
||||
# Cryptography.
|
||||
# AES-256-GCM key for encrypting stored prompts.
|
||||
# MUST be set in production via: VEYLANT_CRYPTO_AES_KEY_BASE64
|
||||
# Generate: openssl rand -base64 32
|
||||
crypto:
|
||||
# Development placeholder — override in production via env var.
|
||||
aes_key_base64: ""
|
||||
|
||||
# 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 limiting defaults. Per-tenant overrides stored in the rate_limit_configs table.
|
||||
rate_limit:
|
||||
default_tenant_rpm: 1000
|
||||
default_tenant_burst: 200
|
||||
default_user_rpm: 100
|
||||
default_user_burst: 20
|
||||
|
||||
# Email notifications via SMTP (Mailtrap sandbox).
|
||||
# Override password in production: VEYLANT_NOTIFICATIONS_SMTP_PASSWORD=<full_password>
|
||||
# Full password visible in Mailtrap dashboard → Sending → SMTP Settings → Show credentials.
|
||||
# Email notifications via SMTP.
|
||||
# Override credentials in production via env vars:
|
||||
# VEYLANT_NOTIFICATIONS_SMTP_HOST
|
||||
# VEYLANT_NOTIFICATIONS_SMTP_PORT
|
||||
# VEYLANT_NOTIFICATIONS_SMTP_USERNAME
|
||||
# VEYLANT_NOTIFICATIONS_SMTP_PASSWORD
|
||||
# VEYLANT_NOTIFICATIONS_SMTP_FROM
|
||||
notifications:
|
||||
smtp:
|
||||
host: "sandbox.smtp.mailtrap.io"
|
||||
host: "smtp.example.com"
|
||||
port: 587
|
||||
username: "2597bd31d265eb"
|
||||
# Mailtrap password — replace ****3c89 with the full value from the dashboard.
|
||||
password: "cd126234193c89"
|
||||
from: "noreply@veylant.ai"
|
||||
username: "alerts@example.com"
|
||||
password: "your-smtp-password"
|
||||
from: "noreply@example.com"
|
||||
from_name: "Veylant IA"
|
||||
67
test_smtp.go
67
test_smtp.go
@ -1,67 +0,0 @@
|
||||
//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()
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
# ─── 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
|
||||
@ -1,53 +0,0 @@
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# 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
|
||||
@ -1,14 +0,0 @@
|
||||
<!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>
|
||||
@ -1,29 +0,0 @@
|
||||
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
2768
web-public/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,29 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
@ -1,24 +0,0 @@
|
||||
// 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);
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
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>
|
||||
);
|
||||
@ -1,382 +0,0 @@
|
||||
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 </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 </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 </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>
|
||||
);
|
||||
}
|
||||
@ -1,80 +0,0 @@
|
||||
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 /> },
|
||||
],
|
||||
},
|
||||
]);
|
||||
@ -1,59 +0,0 @@
|
||||
@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
10
web-public/src/vite-env.d.ts
vendored
@ -1,10 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_DASHBOARD_URL: string;
|
||||
readonly VITE_PLAYGROUND_URL: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
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;
|
||||
@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
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,
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user