fix
This commit is contained in:
parent
279e8f88c3
commit
7c5d9d1225
21
LICENSE
Normal file
21
LICENSE
Normal 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
508
README.md
@ -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 | 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
39
web-public/Dockerfile
Normal 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
|
||||
53
web-public/docker-compose.yml
Normal file
53
web-public/docker-compose.yml
Normal 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
14
web-public/index.html
Normal 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
29
web-public/nginx.conf
Normal 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
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
29
web-public/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
6
web-public/postcss.config.js
Normal file
6
web-public/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
24
web-public/src/lib/utils.ts
Normal file
24
web-public/src/lib/utils.ts
Normal 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
11
web-public/src/main.tsx
Normal 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>
|
||||
);
|
||||
382
web-public/src/pages/LandingPage.tsx
Normal file
382
web-public/src/pages/LandingPage.tsx
Normal 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 </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>
|
||||
);
|
||||
}
|
||||
80
web-public/src/router.tsx
Normal file
80
web-public/src/router.tsx
Normal 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
59
web-public/src/styles.css
Normal 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
10
web-public/src/vite-env.d.ts
vendored
Normal 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;
|
||||
}
|
||||
63
web-public/tailwind.config.ts
Normal file
63
web-public/tailwind.config.ts
Normal 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
25
web-public/tsconfig.json
Normal 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
18
web-public/vite.config.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user